Esta es una versión muy simplificada de una función que uso en una aplicación que se creó hace unos 3 años. Adaptado a la pregunta en cuestión.
-
Encuentra ubicaciones en el perímetro de un punto usando un cuadro . Se podría hacer esto con un círculo para obtener resultados más precisos, pero para empezar solo pretende ser una aproximación.
-
Ignora el hecho de que el mundo no es plano. Mi aplicación solo estaba destinada a una región local, de unos 100 kilómetros de ancho. Y el perímetro de búsqueda solo se extiende por unos pocos kilómetros. Hacer que el mundo sea plano es lo suficientemente bueno para este propósito. (Todo:una mejor aproximación para la relación latitud/longitud según la geolocalización podría ayudar).
-
Funciona con códigos geográficos como los que obtienes de Google Maps.
-
Funciona con PostgreSQL estándar sin extensión (no se requiere PostGis), probado en PostgreSQL 9.1 y 9.2.
Sin índice, habría que calcular la distancia para cada fila de la tabla base y filtrar las más cercanas. Extremadamente caro con mesas grandes.
Editar:
Volví a verificar y la implementación actual permite un índice GisT en puntos (Postgres 9.1 o posterior). Simplificó el código en consecuencia.
El truco principal es usar un índice GiST funcional de cajas , aunque la columna es solo un punto. Esto hace posible usar la implementación de GiST
existente. .
Con una búsqueda tan (muy rápida), podemos obtener todas las ubicaciones dentro de un cuadro. El problema restante:sabemos el número de filas, pero no sabemos el tamaño del cuadro en el que se encuentran. Eso es como saber parte de la respuesta, pero no la pregunta.
Yo uso una búsqueda inversa similar enfoque al descrito con más detalle en esta respuesta relacionada en dba.SE . (Solo que no estoy usando índices parciales aquí, en realidad también podría funcionar).
Iterar a través de una serie de pasos de búsqueda predefinidos, desde muy pequeños hasta "lo suficientemente grandes para contener al menos suficientes ubicaciones". Significa que tenemos que ejecutar un par de consultas (muy rápidas) para llegar al tamaño del cuadro de búsqueda.
Luego busque en la tabla base con este cuadro y calcule la distancia real solo para las pocas filas devueltas del índice. Por lo general, habrá algún excedente ya que encontramos que la caja contiene al menos suficientes ubicaciones. Al tomar los más cercanos, efectivamente redondeamos las esquinas de la caja. Puede forzar este efecto haciendo que la caja sea un poco más grande (multiplique radius
en la función de sqrt(2) para ser completamente preciso resultados, pero no lo haría todo, ya que esto es aproximado para empezar).
Esto sería aún más rápido y sencillo con un SP GiST index, disponible en la última versión de PostgreSQL. Pero no sé si eso es posible todavía. Necesitaríamos una implementación real para el tipo de datos y no tuve tiempo de profundizar en ello. Si encuentras la manera, ¡prométeme volver a informar!
Dada esta tabla simplificada con algunos valores de ejemplo (adr
.. dirección):
CREATE TABLE adr(adr_id int, adr text, geocode point);
INSERT INTO adr (adr_id, adr, geocode) VALUES
(1, 'adr1', '(48.20117,16.294)'),
(2, 'adr2', '(48.19834,16.302)'),
(3, 'adr3', '(48.19755,16.299)'),
(4, 'adr4', '(48.19727,16.303)'),
(5, 'adr5', '(48.19796,16.304)'),
(6, 'adr6', '(48.19791,16.302)'),
(7, 'adr7', '(48.19813,16.304)'),
(8, 'adr8', '(48.19735,16.299)'),
(9, 'adr9', '(48.19746,16.297)');
El índice se ve así:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Tendrás que ajustar el área de la casa, los pasos y el factor de escala a tus necesidades. Mientras busque en cuadros de unos pocos kilómetros alrededor de un punto, una tierra plana es una aproximación lo suficientemente buena.
Necesita comprender bien plpgsql para trabajar con esto. Siento que ya he hecho bastante aquí.
CREATE OR REPLACE FUNCTION f_find_around(_lat double precision, _lon double precision, _limit bigint = 50)
RETURNS TABLE(adr_id int, adr text, distance int) AS
$func$
DECLARE
_homearea CONSTANT box := '(49.05,17.15),(46.35,9.45)'::box; -- box around legal area
-- 100m = 0.0008892 250m, 340m, 450m, 700m,1000m,1500m,2000m,3000m,4500m,7000m
_steps CONSTANT real[] := '{0.0022,0.003,0.004,0.006,0.009,0.013,0.018,0.027,0.040,0.062}'; -- find optimum _steps by experimenting
geo2m CONSTANT integer := 73500; -- ratio geocode(lon) to meter (found by trial & error with google maps)
lat2lon CONSTANT real := 1.53; -- ratio lon/lat (lat is worth more; found by trial & error with google maps in (Vienna)
_radius real; -- final search radius
_area box; -- box to search in
_count bigint := 0; -- count rows
_point point := point($1,$2); -- center of search
_scalepoint point := point($1 * lat2lon, $2); -- lat scaled to adjust
BEGIN
-- Optimize _radius
IF (_point <@ _homearea) THEN
FOREACH _radius IN ARRAY _steps LOOP
SELECT INTO _count count(*) FROM adr a
WHERE a.geocode <@ box(point($1 - _radius, $2 - _radius * lat2lon)
, point($1 + _radius, $2 + _radius * lat2lon));
EXIT WHEN _count >= _limit;
END LOOP;
END IF;
IF _count = 0 THEN -- nothing found or not in legal area
EXIT;
ELSE
IF _radius IS NULL THEN
_radius := _steps[array_upper(_steps,1)]; -- max. _radius
END IF;
_area := box(point($1 - _radius, $2 - _radius * lat2lon)
, point($1 + _radius, $2 + _radius * lat2lon));
END IF;
RETURN QUERY
SELECT a.adr_id
,a.adr
,((point (a.geocode[0] * lat2lon, a.geocode[1]) <-> _scalepoint) * geo2m)::int4 AS distance
FROM adr a
WHERE a.geocode <@ _area
ORDER BY distance, a.adr, a.adr_id
LIMIT _limit;
END
$func$ LANGUAGE plpgsql;
Llamar:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Devuelve una lista de $3
ubicaciones, si hay suficientes en el área de búsqueda máxima definida.
Ordenados por distancia real.
Más mejoras
Cree una función como:
CREATE OR REPLACE FUNCTION f_geo2m(double precision, double precision)
RETURNS point AS
$BODY$
SELECT point($1 * 111200, $2 * 111400 * cos(radians($1)));
$BODY$
LANGUAGE sql IMMUTABLE;
COMMENT ON FUNCTION f_geo2m(double precision, double precision)
IS 'Project geocode to approximate metric coordinates.
SELECT f_geo2m(48.20872, 16.37263) --';
Las (literalmente) constantes globales 111200
y 111400
están optimizados para mi área (Austria) desde la Longitud de un grado de longitud
y La longitud de un grado de latitud
, pero básicamente solo funciona en todo el mundo.
Úselo para agregar un código geográfico escalado a la tabla base, idealmente una columna generada como se describe en esta respuesta:
¿Cómo se hacen las matemáticas de fechas que ignoran el año?
Consulte 3. Versión de magia negra donde lo guío a través del proceso.
Luego puede simplificar la función un poco más:Escale los valores de entrada una vez y elimine los cálculos redundantes.