Saltar al contenido
geoespacial / · 6 min

Geocodificación inversa con PostGIS: en qué colonia y en qué sección cayó ese punto

Una función de veinte líneas que recibe latitud y longitud y devuelve colonia, código postal, municipio con clave INEGI, sección electoral y distrito. Sin API de terceros y sin costo por consulta.

Administrador · Desarrollador Laravel
Geocodificación inversa con PostGIS: en qué colonia y en qué sección cayó ese punto

Alguien manda su ubicación por WhatsApp. Llegan dos números: 20.1234, -98.7654.

Lo que necesitas es «Colonia Céntro, CP 42000, Pachuca de Soto, sección 1183, distrito local 8». Y lo necesitas sin pagar por consulta a un servicio externo, porque en un mes de campaña son decenas de miles.

Eso se resuelve con PostGIS y una función de veinte líneas. Este es el camino completo.

Lo que hace falta

PostgreSQL con PostGIS. En Windows se instala con el Stack Builder que viene con PostgreSQL: Application Stack Builder → tu servidor → Spatial Extensions → PostGIS Bundle. Eso agrega la extensión y, lo que más importa, el ejecutable shp2pgsql.exe.

Los shapefiles. El INEGI publica el marco geoestadístico con municipios, localidades, AGEB y manzanas. El INE publica la cartografía electoral con secciones y distritos. Las colonias suelen venir del catálogo estatal o del municipio.

Son archivos .shp con sus acompañantes .dbf, .shx y .prj. Hay que importarlos.

Importar los shapefiles, con las dos banderas que importan

shp2pgsql -s 32614:4326 -W LATIN1 -I -D -g geom colonias.shp public.colonias | psql -d mi_base

Cada bandera de esa línea me costó tiempo descubrirla:

-s 32614:4326 reproyecta. Los shapefiles oficiales mexicanos vienen en UTM zona 14N (EPSG:32614), que mide en metros sobre una proyección plana. Tus puntos de GPS vienen en WGS84 (EPSG:4326), que son grados de latitud y longitud. Si no reproyectas al importar, ST_Contains no encuentra nada nunca —y no da error, simplemente devuelve vacío, que es la peor forma de fallar.

Los dos números son origen y destino. Y hay que verificar el origen en el .prj del shapefile: no todos los estados publican en la misma zona UTM. México abarca de la zona 11 a la 16.

-W LATIN1 es la codificación. Los shapefiles oficiales vienen en Latin-1, no en UTF-8. Sin esa bandera, cada «Ñ» y cada acento entra roto en la base, y lo descubres cuando alguien busca «Peñitas» y no aparece.

-I crea el índice espacial GIST. Sin él, cada consulta es un recorrido completo de la tabla. Con miles de polígonos, la diferencia es de milisegundos a segundos.

-D usa el formato de volcado en lugar de INSERT uno por uno. En una importación de cincuenta mil manzanas, es la diferencia entre minutos y una hora.

-g geom nombra la columna de geometría. Sin eso queda como geom de todos modos en versiones recientes, pero prefiero ser explícito.

La función

DROP FUNCTION IF EXISTS geocodificar(double precision, double precision);

CREATE FUNCTION geocodificar(p_lat double precision, p_lon double precision)
RETURNS TABLE (
  colonia text, cp text, municipio text,
  municipio_cod int, seccion text, distrito_l text
)
LANGUAGE sql
STABLE
AS $$
  SELECT
    c.nombre::text                                       AS colonia,
    c.cp::text                                           AS cp,
    m.nombre::text                                       AS municipio,
    COALESCE(m.municipio, c.municipio, s.municipio)::int AS municipio_cod,
    s.seccion::text                                      AS seccion,
    s.distrito_l::text                                   AS distrito_l
  FROM (SELECT ST_SetSRID(ST_MakePoint(p_lon, p_lat), 4326) AS g) p
  LEFT JOIN colonias   c ON ST_Contains(c.geom, p.g)
  LEFT JOIN secciones  s ON ST_Contains(s.geom, p.g)
  LEFT JOIN municipios m ON ST_Contains(m.geom, p.g)
  LIMIT 1;
$$;

Y se usa así:

SELECT * FROM geocodificar(20.1234, -98.7654);

Los detalles que no son obvios

ST_MakePoint(p_lon, p_lat) — longitud primero. Es el error más común de PostGIS y no da error: te devuelve resultados de un punto en otro continente. En coordenadas geográficas el orden es X, Y, o sea longitud, latitud, al revés de como las dice todo el mundo. Fíjate en que los parámetros de la función sí van (lat, lon), porque es como llegan de un GPS; el volteo se hace adentro, una sola vez, donde no se olvida.

ST_SetSRID(..., 4326) le dice a PostGIS en qué sistema de referencia está ese punto. Un punto sin SRID no se puede comparar con un polígono que sí lo tiene.

LEFT JOIN y no JOIN. Un punto puede caer dentro de una sección electoral pero fuera de cualquier colonia mapeada — pasa constantemente en zonas rurales y en fraccionamientos nuevos. Con JOIN la consulta devuelve cero filas y pierdes también el dato de la sección. Con LEFT JOIN devuelves lo que sí sabes y NULL lo que no.

El COALESCE de tres fuentes para la clave del municipio. Las tres capas traen la clave INEGI, y cada una tiene huecos distintos. Se toma la primera que esté.

STABLE le dice a PostgreSQL que la función devuelve lo mismo dentro de una misma consulta. Eso permite optimizarla cuando se llama desde un SELECT sobre muchas filas. No es IMMUTABLE porque lee tablas.

El truco para poder crearla antes que las tablas

SET check_function_bodies = false;

Por omisión, PostgreSQL valida el cuerpo de una función al crearla, así que si colonias todavía no existe, el CREATE FUNCTION falla.

Con esa línea, el cuerpo se valida en tiempo de ejecución. Eso permite que el script de esquema corra completo en una base vacía y los shapefiles se importen después, en cualquier orden. Sin ella, el orden de instalación se vuelve frágil y alguien va a tropezar con él.

Cuánto tarda

Con el índice GIST, una consulta de este tipo sobre unos miles de polígonos está en el orden de milisegundos. Es más rápido que la llamada HTTP que harías a un servicio externo, y no tiene cuota.

Si lo vas a llamar sobre una tabla entera —geocodificar cien mil registros históricos—, hazlo por lotes y con un WHERE que salte lo ya resuelto, para poder interrumpirlo y retomarlo:

UPDATE reportes r
SET colonia = g.colonia, seccion = g.seccion
FROM geocodificar(r.lat, r.lon) g
WHERE r.colonia IS NULL
  AND r.id IN (SELECT id FROM reportes WHERE colonia IS NULL LIMIT 5000);

Lo que gana más allá de la dirección

El dato que de verdad vale no es la colonia: es la sección electoral.

Es la unidad territorial mínima del país, tiene entre cien y tres mil electores, y es la llave con la que se cruzan los resultados electorales históricos, las estadísticas censales del INEGI por AGEB y cualquier padrón propio.

Un punto convertido en sección deja de ser una dirección y se vuelve una fila que puedes cruzar con todo lo demás. Esa conversión es la que hace que una plataforma territorial sirva para algo más que pintar puntos en un mapa.

#postgresql #ine #postgis #inegi #sql #mapas
Comentarios 0

Nadie ha comentado todavía. Estrena la sección.

Deja tu comentario
Tu correo no se publica. Reviso los comentarios antes de publicarlos.

Seguir leyendo