POI Searches Just Got Up To 20× Faster

TechnicalNews

A route into Miami covered in points of interest found by a single PostGIS corridor query

PitStopper's POI searches now run on a new engine. If you searched a route today, your results came back from a single indexed PostGIS corridor query - usually in about a second - instead of the fleet of Overpass queries the app used to send. Long routes that previously spun for minutes (or never finished at all) now complete in a few seconds. This post is about what changed, the measurements behind it, and the road that got us here.

The Problem With Long Routes

Overpass answers a POI search one bounding box at a time. For a short walk that's fine - one box, one category group, one query. But a long route gets split into many boxes to stay within Overpass's limits, and each of those boxes has to be queried separately for every category group you've selected. Tick 20 categories on a 700km route like London-Edinburgh and you're not making one search, you're making dozens - each with its own 25-second timeout, each competing for the same server slot.

In practice that meant long-route searches could spin for minutes, get benched by cooldowns after a timeout, or simply never finish - even against our own self-hosted mirror at o.pitstopper.net. The server was never the bottleneck; the query pattern was.

What We Did First: Cache-First

Before touching the query pattern itself, we went after the queries we didn't need to make at all. The app never re-fetches data it already holds - re-ticking a category, reducing the search radius, or re-opening a saved project are all served locally from cache instead of hitting the network again. That cut out a lot of unnecessary queries. The new engine is about the queries that are genuinely necessary - the first-ever search for a given route and category set.

The New Approach: One Corridor Query

Instead of boxes × categories, every search is now a single PostGIS query: buffer the whole route geometry by the search radius to get a "corridor," subdivide that corridor with ST_Subdivide so the planner can use spatial indexes efficiently, and join it against every selected category in one indexed request. One route, one query, regardless of how many categories or how long the route is.

The difference is easiest to see. This is the same ~144km route with the app's "Show Search Area" view (System menu) on each backend - first the old Overpass bounding-box grid, where every tinted box was queried separately for every category group:

The Overpass approach: the route's area carved into large bounding boxes, each queried separately

And the PostGIS corridor - the green band hugging the route is the entire queried area, fetched for every selected category in one request:

The PostGIS approach: a single buffered corridor following the route, one query for everything

We tried corridors on Overpass first - and it completely failed

Overpass does have a corridor-shaped tool: the around: filter accepts a polyline, "everything within X metres of this line." We tried it, and for real routes it fell apart for two compounding reasons.

First, the route geometry has to be pasted into the query as text - every coordinate, spelled out - and repeated for every tag filter in the query. A long route is thousands of points; multiply by 20 categories' worth of filters and the query itself balloons towards megabytes, which Overpass servers reject outright before they've even looked at the map.

Second, even when a slimmed-down query got through, Overpass has no spatial index for "distance to a line." It checks candidate elements against the route segment by segment, so the work grows with route length × element density - and long-route queries blew straight through the 25-second timeout. The only place the technique survived in the app was the Hazards scan: a very narrow 75m corridor for a handful of tags, and even that took ~15 seconds on a 180km route.

PostGIS solves both problems structurally. The route geometry travels as data (not query text), gets buffered into a corridor once, and ST_Subdivide chops that corridor into small cells the spatial index can answer directly - so the database only ever examines elements that are plausibly near the route. Same idea, but with the geometry engine and indexes the idea always needed. (That ~15-second hazards scan now runs in about 2.)

We prototyped this against postpass, Frederik Ramm's open-source SQL-over-PostGIS wrapper for OpenStreetMap data, using the public planet instance at postpass.geofabrik.de run by Geofabrik. Their permissive CORS policy is what made a zero-infrastructure prototype possible at all - we could validate the whole design against a full planet database straight from the browser, before committing to any server of our own. With the design proven, we built exactly that server: a planet-wide PostGIS database filtered down to just the ~66 OpenStreetMap tag pairs PitStopper's categories actually use, which shrinks a planet's worth of data to a very manageable size. No reason to host every building outline to answer questions about cafes and water taps.

Why the query planner matters

Our first prototype filtered on tags->>'key' = 'value', which looks perfectly reasonable but can't use the GIN indexes on the tags column. The planner fell back to a sequential scan of a planet-sized table - EXPLAIN showed a plan cost around 4.5 billion. Switching to jsonb containment (tags @> '{"key":"value"}') plus a bounding-box prefilter let the planner use the index properly, and the same query's plan cost dropped to roughly 4,600 - six orders of magnitude lower. The lesson, not for the first time: EXPLAIN is your friend, and postpass conveniently exposes it as an API endpoint (/api/explain) so you can check your query's plan before you run it for real.

The safety net

Fast is worthless if it's fragile, so the new engine fails safe - twice over. Searches go to our own PostGIS server first; if it ever has a bad moment, the search transparently degrades to the public postpass instance, and if that fails too, the classic Overpass path takes over mid-search. Circuit breakers bench a misbehaving tier so nobody sits through repeated timeouts, and we monitor every degradation so we know about problems before you tell us. Nobody gets stranded with an empty result.

The Results

We measured both backends against the same routes and categories on our staging site, cold (first-ever query, no cache warm on either side):

Scenario Overpass (cold) PostGIS (cold) Speedup
Chilterns walk ~6km, 20 categories 7.1s 0.21s 34×
Fred Whitton ~180km, 8 categories 9.7s 0.66s 15×
Fred Whitton ~180km, 20 categories 20.1s 0.85s 24×
London-Edinburgh ~700km, 1 category 36.4s 2.2s 17×
London-Edinburgh ~700km, 8 categories did not finish (120s timeout) 2.8s (449 POIs) -
London-Edinburgh ~700km, 20 categories did not finish (120s timeout) 4.1s (1,989 POIs) -
Around-a-point 5km, 8 categories 5.4s 1.6s

Result parity was 97-100% on every scenario both backends could complete - the small differences come down to data freshness and how each side measures POIs right at the edge of the search radius, not missing data.

The Route in the Pictures

The Miami route in the header image (and both "Show Search Area" screenshots above) later became a benchmark scenario of its own, and it's the cleanest comparison in the whole exercise: it was brand new to both systems when measured, so neither side had any cache to lean on. These numbers are exactly what a first-time search feels like:

Miami-Homestead ~144km Overpass (cold) PostGIS (cold) Speedup
1 category 5.3s 1.1s
8 categories 14.0s 0.72s 19×
20 categories 27.8s 1.6s 18×

That's the header image's search - 697 POIs along 144km - dropping from a 14-second spinner to under a second.

We picked this route for a second reason: those long dead-straight grid roads around Homestead are the worst case for corridor construction (route simplification reduces a straight road to just its two endpoints). So we compared what was found, not just how fast: 798 vs 795 POIs on the heaviest search, with 100% id-level agreement once radius-edge placement differences are accounted for. The corridor genuinely covers the whole route - straights included.

What The Speed Unlocks

Some features only make sense when a search costs a second instead of half a minute. The first one shipped alongside the engine: unticked categories in the POI panel now show a dimmed ≈N estimate of how many POIs each would find along your route - computed by a single count query when your route loads, so you know what's out there before selecting anything. The Hazards & Obstacles and surface-analysis features run on the same corridor approach too.

Fair Caveats

This isn't a one-sided story. Overpass wins outright on an identical repeated query, because our mirror caches responses for 48 hours and can answer in tens of milliseconds - but the cache-first client rarely asks the same question twice, so that advantage matters less in practice than it looks on paper. And our own server carries a response cache of its own now, so repeats are fast on both paths.

If you prefer the classic behaviour, the old engine is one switch away: Settings → Preferences lets you turn the PostGIS backend off and search via Overpass exactly as before.

What's Next

Our server currently answers the core route and location searches; the more specialist lookups (way geometries for hazards, open-ended "what's here?" queries) still use the public postpass instance while our server grows those endpoints. Data refreshes weekly from OpenStreetMap. And with searches this cheap, there's a queue of ideas we can now afford: "longest stretch without water" warnings, name search along a route, resupply-cluster detection.

Thanks

None of this would exist without the OpenStreetMap community, whose mapping makes all of this data possible in the first place; Frederik Ramm, for building and open-sourcing postpass; and Geofabrik, for running the public postpass planet instance whose CORS policy let us validate this idea for free before committing to our own infrastructure - and which still backs some of our specialist lookups today. If you find postpass useful, the project is on GitHub.