Esta é uma versão amplamente simplificada de uma função que uso em um aplicativo criado há cerca de 3 anos. Adaptado à questão em questão.
-
Encontra locais no perímetro de um ponto usando uma caixa . Pode-se fazer isso com um círculo para obter resultados mais precisos, mas isso é apenas uma aproximação para começar.
-
Ignora o fato de que o mundo não é plano. A minha candidatura destinava-se apenas a uma região local, com cerca de 100 quilómetros. E o perímetro de busca abrange apenas alguns quilômetros. Tornar o mundo plano é bom o suficiente para esse propósito. (Todo:Uma melhor aproximação para a relação lat/lon dependendo da geolocalização pode ajudar.)
-
Opera com geocódigos como você obtém do Google Maps.
-
Funciona com PostgreSQL padrão sem extensão (não requer PostGis), testado no PostgreSQL 9.1 e 9.2.
Sem índice, seria preciso calcular a distância para cada linha da tabela base e filtrar as mais próximas. Extremamente caro com grandes mesas.
Editar:
Verifiquei novamente e a implementação atual permite um índice GisT em pontos (Postgres 9.1 ou posterior). Simplificou o código de acordo.
Com uma busca tão (muito rápida), podemos obter todos os locais dentro de uma caixa. O problema restante:sabemos o número de linhas, mas não sabemos o tamanho da caixa em que estão. É como saber parte da resposta, mas não a pergunta.
Eu uso uma pesquisa reversa semelhante abordagem descrita em mais detalhes em esta resposta relacionada em dba.SE . (Apenas, não estou usando índices parciais aqui - pode realmente funcionar também).
Itere através de uma série de etapas de pesquisa pré-definidas, desde muito pequenas até "grandes o suficiente para conter pelo menos locais suficientes". Significa que temos que executar algumas consultas (muito rápidas) para chegar ao tamanho da caixa de pesquisa.
Em seguida, pesquise a tabela base com esta caixa e calcule a distância real apenas para as poucas linhas retornadas do índice. Geralmente haverá algum excedente desde que encontramos a caixa com pelo menos locais suficientes. Ao pegar os mais próximos, efetivamente arredondamos os cantos da caixa. Você pode forçar este efeito tornando a caixa um entalhe maior (multiplique
radius
na função por sqrt(2) para ficar completamente preciso resultados, mas eu não faria tudo, já que isso está se aproximando para começar). Isso seria ainda mais rápido e simples com um SP GiST index, disponível na última versão do PostgreSQL. Mas ainda não sei se isso é possível. Precisaríamos de uma implementação real para o tipo de dados e não tive tempo de mergulhar nisso. Se você encontrar uma maneira, prometa que relata!
Dada esta tabela simplificada com alguns valores de exemplo (
adr
.. Morada):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)');
O índice fica assim:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
-> SQLfiddle
Você terá que ajustar a área da casa, as etapas e o fator de escala às suas necessidades. Contanto que você procure em caixas de alguns quilômetros ao redor de um ponto, uma terra plana é uma boa aproximação.
Você precisa entender bem o plpgsql para trabalhar com isso. Sinto que já fiz o bastante aqui.
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;
Ligar:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Retorna uma lista de
$3
locais, se houver o suficiente na área de pesquisa máxima definida.Classificado pela distância real.
Mais melhorias
Construa uma função 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) --';
As constantes globais (literalmente)
111200
e 111400
são otimizados para minha área (Áustria) a partir do Comprimento de um grau de longitude
e O comprimento de um grau de latitude
, mas basicamente só funciona em todo o mundo. Use-o para adicionar um geocódigo dimensionado à tabela base, de preferência uma coluna gerada como descrito nesta resposta:
Como você data matemática que ignora o ano?
Consulte 3. Versão de magia negra onde eu acompanho você pelo processo.
Então você pode simplificar um pouco mais a função:Escale os valores de entrada uma vez e remova cálculos redundantes.