Reverse Geocoding: Turning Coordinates Back Into an Address
9 min read · updated August 11, 2026
There is no table mapping coordinates to addresses. A reverse geocoder runs a nearest-feature search and a stack of containment tests, and almost everything surprising about its output follows from that.
It is not a lookup
Nominatim, the reverse geocoder behind OpenStreetMap, states the mechanism plainly in its reverse API documentation: it does not compute an address for the exact coordinate, it works “by finding the closest suitable OSM object and returning its address information”. Commercial services differ in their data and their tie-breaking, not in the shape of the computation.
That single sentence explains the behaviour people file as bugs. Stand in the middle of a park and you get the address of whatever the geocoder considers the nearest addressable thing, which may be across the road. Stand in a courtyard behind a terrace and you get one of the houses, chosen by distance rather than by which one owns the courtyard. The result is not an assertion that you are at that address; it is an assertion that this was the closest object worth naming.
The point-in-polygon stack
The administrative part of the answer — country, region, city, suburb — is genuine containment, and this is where point-in-polygon runs. Take the coordinate 52.3738, 4.8910. The query is: which polygons in the boundary table contain this point?
Done naively that is a ray-casting test against every polygon in the world. The test itself is cheap — cast a ray in one direction from the point, count how many polygon edges it crosses, odd means inside — but a national boundary can hold a hundred thousand vertices and there are millions of polygons. So the work happens in two stages, and the first stage is the index:
-- PostGIS: the && operator is the bounding-box test, index-backed. -- ST_Contains is the exact test, run only on what survives it. SELECT b.name, b.admin_level FROM boundaries b WHERE b.geom && ST_SetSRID(ST_MakePoint(4.8910, 52.3738), 4326) AND ST_Contains(b.geom, ST_SetSRID(ST_MakePoint(4.8910, 52.3738), 4326)) ORDER BY b.admin_level DESC;
The && operator compares bounding boxes using a GiST index, cutting millions of candidates to a handful in logarithmic time. Only those few get the exact ray-casting test. Reverse the two and the query is thousands of times slower for an identical answer; this is the whole reason the geometry column is indexed.
Ordering by admin_level descending gives you the hierarchy from the inside out: neighbourhood, then municipality, then province, then country. Note the coordinate order in ST_MakePoint — longitude first. That is the convention GeoJSON and PostGIS share and the opposite of how humans say it, and swapping them is the most common single bug in this entire field. A swapped pair for Amsterdam lands at 4.89°N 52.37°E, in the Indian Ocean, and the reverse geocoder will cheerfully return nothing rather than an error.
The nearest-feature search
The street and house number come from a different query: a nearest-neighbour search over ways and address points. The pattern in PostGIS is an index-ordered scan using the distance operator, which lets the index walk features in order of proximity rather than computing every distance:
SELECT w.name,
ST_Distance(w.geom::geography, p.geom::geography) AS metres,
ST_LineLocatePoint(w.geom, p.geom) AS fraction
FROM ways w,
(SELECT ST_SetSRID(ST_MakePoint(4.8910, 52.3738), 4326) AS geom) p
WHERE w.name IS NOT NULL
ORDER BY w.geom <-> p.geom
LIMIT 5;ST_LineLocatePoint returns where along the street the perpendicular foot falls, as a fraction from 0 to 1. If the segment carries address ranges — the same interpolation data a forward geocoder uses — that fraction converts straight back into a house number. A fraction of 0.51 on a segment numbered 100 to 198 gives 100 + 0.51 × 98 = 150, rounded to the nearest number of the correct parity. Forward and reverse geocoding on the same street are the same arithmetic run in opposite directions, which is why a round trip through both usually returns you to a slightly different coordinate than you started with.
Two details matter in that query. The cast to geography is what makes metres mean metres; without it PostGIS returns degrees, and a degree is 111 km of latitude and anywhere from 111 km to zero of longitude depending where you are. And the ordering uses <->, the index-assisted distance operator, on the unprojected geometry — fast and slightly wrong at high latitude — while the reported distance is computed properly on the few rows that survive. That split is deliberate: approximate to shortlist, exact to answer.
Choosing how much detail to return
A reverse geocode has to decide how specific to be, and Nominatim exposes that as a zoom parameter borrowed from tile zoom levels. Its documented mapping runs 3 for country, 5 for state, 8 for county, 10 for city, 12 for town or borough, 13 for village or suburb, 14 for neighbourhood, 15 for any settlement, 16 for major streets, 17 for major and minor streets, and 18 — the default — for a building.
This is not cosmetic. At zoom 18 a coordinate in a car park returns the nearest building, several tens of metres away and semantically wrong. At zoom 16 the same coordinate returns the street, which is both closer to the truth and more useful for display. If you are reverse geocoding GPS traces to label where a vehicle stopped, asking for a building is asking the service to guess; asking for a street is asking it for something it can actually support.
Where it goes wrong
- Nearest is not containing. The nearest addressable object to a point in the middle of a wide road can be on either side. If the answer has to be the correct side — a delivery, a pickup point — that is a separate problem with its own geometry; see side-of-road detection.
- Boundary points are genuinely ambiguous. A coordinate on a shared border satisfies the containment test for both polygons, or neither, depending on floating-point rounding of two independently digitised lines. Ray casting has no tolerance parameter, so the answer flips on the last bit of a double.
- Sparse data returns a distant answer confidently.Nominatim’s own documentation warns that results can be unexpected where the nearest object has a dissimilar address or where small unnamed features are simply absent. The response format is identical whether the match was 3 metres away or 3 kilometres, so read the distance rather than trusting the string.
- Multi-polygon exclaves and holes. A city with a hole in it — an independent enclave — is stored as a polygon with an interior ring, and a naive containment implementation that ignores interior rings gets the enclave wrong every time.
Coming the other way, the accuracy tier attached to a forward geocode tells you how much of that round trip you should trust: see what rooftop, interpolated and ZIP-level mean.