Designing a fast spatial search API on PostGIS
REST API design · Database design · PostGIS · Query optimisation · GIS data processing
01Problem
A map-based product needed to answer “what points of interest are near this location?” and “what is inside this area?” for a growing dataset, with results returned quickly enough to feel instant as the user pans. POI data came from OpenStreetMap extracts and internal sources with different schemas.
02Architecture
A single Spring Boot service owns the POI domain. PostgreSQL with PostGIS is the source of truth; a scheduled import pipeline normalises source data into it. Clients (Angular admin, mobile/web map) talk only to the REST API through an Nginx reverse proxy.
- Geometry stored as geography(Point, 4326) so distance queries are in metres without manual reprojection.
- GiST index on the geometry column; B-tree indexes on category and status for combined filters.
- Read endpoints are stateless and cacheable at the proxy for identical bounding boxes.
03Implementation
Spatial queries are written as native queries in Spring Data repositories rather than forced through JPQL, keeping the SQL explicit and easy to EXPLAIN. Bounding-box requests use ST_MakeEnvelope + the && operator to hit the index first, then ST_DWithin for exact radius filtering.
SELECT id, name, category, ST_Distance(geom, ST_MakePoint(:lng, :lat)::geography) AS distance_m FROM poi WHERE status = 'ACTIVE' AND ST_DWithin(geom, ST_MakePoint(:lng, :lat)::geography, :radius_m) ORDER BY distance_m LIMIT :limit;04Challenges
The obvious approach worked on a small table and fell over as data grew.
- ORDER BY distance over a large radius forced full sorts — solved by tightening the radius server-side and paginating with a distance cursor.
- Importing multi-gigabyte OSM extracts through the ORM was slow and memory-hungry — replaced with streaming parsing and batched COPY-style inserts.
- Mixed SRIDs from different sources produced silently wrong distances until validation was added at import time.
05Solution
Explicit SQL with the right index types, a streaming import pipeline with idempotent upserts, and API contracts that constrain what clients can ask for (max radius, max page size). Validation happens once at the boundary, so the query path stays simple.
06Result
Nearby and area queries stayed responsive as the dataset grew, and imports became a routine scheduled job instead of a manual event. [PLACEHOLDER] Add measured numbers here if you have them — e.g. p95 latency before/after, dataset size, import time.