As part of the Multi-API Routing & Fallback Chains architecture, reverse geocoding is the direction most likely to dominate your bill and least likely to be designed for. Forward geocoding runs once per address; reverse geocoding runs once per position report, and position reports arrive continuously. This page covers the workflow that keeps that volume affordable — precision targeting, redundancy compression, cell-based caching and result validation.
The single most important decision comes first and is easy to skip: what precision does the answer actually need? A delivery confirmation needs a building; a driver activity report needs a street; a tax or coverage report needs an administrative area. Each answer has a different cost, a different cache strategy, and in the administrative case, no external call at all.
Prerequisites
Precision Decides Everything Downstream
The first row deserves attention because it removes the external call entirely. If the question is “which country is this vehicle in” or “which service area does this stop fall in”, a point-in-polygon test against boundary data answers it in microseconds, offline, with no quota and no provider dependency. A surprising share of reverse-geocoding spend is administrative questions being asked of a rooftop-precision API.
Step 1 — Compress Before You Look Up
Position streams are enormously redundant. A parked vehicle reports the same location every thirty seconds; a vehicle in traffic moves a few metres between reports. Filtering by movement is the single largest reduction available, and it happens before any cache is consulted.
from __future__ import annotations
from dataclasses import dataclass
from math import asin, cos, radians, sin, sqrt
@dataclass(frozen=True)
class Ping:
device_id: str
lat: float
lon: float
ts: float
def haversine_m(a_lat: float, a_lon: float, b_lat: float, b_lon: float) -> float:
"""Great-circle distance in metres."""
p1, p2 = radians(a_lat), radians(b_lat)
dp, dl = p2 - p1, radians(b_lon - a_lon)
h = sin(dp / 2) ** 2 + cos(p1) * cos(p2) * sin(dl / 2) ** 2
return 2 * 6_371_000 * asin(sqrt(h))
def compress(pings: list[Ping], min_move_m: float = 60.0) -> list[Ping]:
"""Keep only pings that moved meaningfully from the last kept position."""
kept: list[Ping] = []
last: dict[str, Ping] = {}
for p in sorted(pings, key=lambda x: (x.device_id, x.ts)):
prev = last.get(p.device_id)
if prev is None or haversine_m(prev.lat, prev.lon, p.lat, p.lon) >= min_move_m:
kept.append(p)
last[p.device_id] = p
return kept
Choose min_move_m from the precision target, not from intuition. For street-level answers
a sixty-metre threshold removes most redundancy without ever changing the answer, because
two points sixty metres apart on the same road reverse-geocode identically. For
building-level answers the threshold must be smaller, and the compression ratio falls
accordingly.
Step 2 — Cache by Cell, Not by Coordinate
Caching on an exact coordinate is close to useless: floating-point positions almost never repeat. Rounding the coordinate to a cell whose size matches the precision target turns a zero-hit cache into one that answers the large majority of queries.
import redis
CELL_PRECISION = {"area": 5, "street": 7, "building": 8}
def cache_key(lat: float, lon: float, target: str, provider: str) -> str:
"""Cell-based cache key — nearby coordinates collapse onto one entry."""
from geohash_encoder import encode # see the geohash guide for an encoder
cell = encode(lat, lon, CELL_PRECISION[target])
return f"rev:v2:{provider}:{target}:{cell}"
def get_or_reverse(r: redis.Redis, lat: float, lon: float, target: str,
provider: str, ttl_s: int = 30 * 86_400) -> dict:
key = cache_key(lat, lon, target, provider)
cached = r.get(key)
if cached is not None:
return json.loads(cached)
result = call_provider(lat, lon, target) # your dispatch layer
r.set(key, json.dumps(result), ex=ttl_s)
return result
The cell size is a correctness decision as well as an economic one. A cell that is too coarse returns the address of a neighbouring building for every point in it, and the error is invisible — a plausible address, wrong by one door. Sizing the cell below the precision you are claiming keeps that from happening.
Step 3 — Validate the Answer Against the Question
Every reverse geocoder returns something. The interesting question is how far away that something is, because a provider with no nearby address will happily return one several hundred metres off with no hint that it did.
Most providers echo the coordinate of the address they matched. Computing the haversine distance between that and your query point costs nothing and converts an unqualified answer into a measured one — the same idea as detecting geocoding outliers, applied in the opposite direction.
Performance and Batching
After compression and caching, the surviving lookups are dispatched exactly like forward geocoding: concurrently, under a rate limiter, with the resilience stack around each call. The numbers below are a representative reduction for a two-hundred-vehicle fleet.
| Stage | Coordinates | Reduction |
|---|---|---|
| raw pings, 30 s interval | 180,000 | — |
| after movement compression at 60 m | 41,000 | −77% |
| after cell cache lookup | 6,200 | −85% of the remainder |
| billable provider calls | 6,200 | 3.4% of raw |
Both reductions come from the same insight: the question being asked repeats far more often than the answer changes. That is also why the cache TTL can be long — a street name at a given cell is stable for months, and the freshness rules that apply to forward geocoding are much weaker here.
Storing the Result Beside the Track
Where the resolved address lives matters as much as how it was obtained. A common mistake is to write the address back onto every position row, which multiplies storage by the ping count and makes a later re-resolution a full table rewrite. Storing the cell-to-address mapping in its own table and joining on demand keeps both the storage and the update cost proportional to places rather than to reports.
That separation also makes provenance possible. The mapping table can carry the provider that answered, the distance between the query point and the returned address, the cell precision in force and the time of resolution — none of which belongs on a position row, and all of which you will want when a route report looks wrong three months later.
There is a retention argument too. Position tracks are usually subject to a shorter retention policy than business records, because they are personal data about a driver’s movements. A design that embeds addresses in the track forces the address history to expire with it; a design that keeps them separate lets the track be deleted on schedule while the delivery record survives with the address it needed.
Finally, keeping the mapping separate makes the cache and the database mutually reinforcing. The mapping table is a durable version of the same information the cache holds, so a cold cache can be warmed from it in one query rather than by re-calling the provider — the same relationship described for Redis and Postgres caching patterns on the forward path.
Choosing Between a Provider and a Local Answer
Reverse geocoding is unusual in that a good local implementation is genuinely competitive with a commercial provider for many workloads. The deciding factors are coverage in the markets you actually operate in, the precision you need, and how much operational appetite you have for maintaining a dataset.
For administrative answers the local option wins outright: boundary data is small, stable, freely available for most of the world, and answers in microseconds. For street-level answers a self-hosted service is competitive where road geometry is well mapped. For building-level answers the question turns on whether an authoritative address-point file exists for your market — where it does, local snapping is dramatically cheaper at fleet volumes; where it does not, a provider is the only realistic option.
Most mature pipelines end up running both, with the local path first and a provider behind it for the tail. That arrangement gets the cost profile of the local option and the coverage of the commercial one, at the price of maintaining two code paths — which the fallback chain already models, so the marginal complexity is small.
Whichever arrangement you choose, decide it per precision target rather than once for the whole pipeline. The right answer for an administrative query is frequently different from the right answer for a rooftop one, and treating them as a single decision forces one of them to be wrong.
Edge Cases
Coordinates on a boundary. A point on a municipal boundary can legitimately reverse to either side, and small GPS jitter will flip it back and forth. Where the answer feeds billing or jurisdiction, snap the answer per device per day rather than per ping so it does not oscillate.
Zero-zero and null island. A failed GPS fix commonly reports (0, 0). Filter it before
the lookup, or you will pay to learn about the Gulf of Guinea repeatedly.
Ocean and airspace coordinates. Vessels and aircraft produce positions with no address anywhere nearby. Detect them by the validation distance and return an area-level answer rather than a nonsensical street.
Very high ping frequency. Some trackers report every second. Compress on time as well as distance — one lookup per device per minute is almost always enough, and the extra pings add nothing but cost.
Privacy considerations. Position histories are personal data in most jurisdictions. Cache the cell, never the device identifier alongside it, and keep retention on the compressed track separate from retention on the address results.
The Reduction, End to End
Troubleshooting
Cache hit rate near zero. The key is built from an unrounded coordinate somewhere — usually a float formatted into the key directly rather than passed through the cell encoder.
Addresses change between identical positions. Two providers are answering, or the cache key omits the provider. Include it: their answers differ, and mixing them produces a track that appears to teleport between conventions.
Spend rising with flat fleet size. Ping frequency changed, or the movement threshold was lowered. Both show up immediately in the compression ratio, which is worth emitting as a metric for exactly this reason.
FAQ
Why is reverse geocoding volume so much higher than forward geocoding volume?
Forward geocoding happens once per address; reverse geocoding happens once per position report. A fleet of 200 vehicles reporting every 30 seconds produces around 180,000 coordinates a day against a few thousand addresses, so the same pipeline can be dominated by the reverse side even though the address count is small.
What cell size should a reverse-geocoding cache use?
Match it to the precision you need. A seven-character geohash covers about 150 metres and suits street-level answers; eight characters covers about 40 metres and suits building-level answers. Too coarse returns the neighbour’s address, too fine caches nothing.
How do I know the returned address is actually correct?
Measure the distance from the query coordinate to the coordinate the provider returns for that address. A large gap means the provider snapped to something further away than you intended — usually the nearest known address rather than the place you asked about.
Should reverse geocoding use the same provider as forward geocoding?
Not necessarily. Coverage and pricing differ between the two operations, and reverse volume is usually far higher, so the economics can favour a different provider or a self-hosted service. Keep the routing decision separate rather than assuming one provider serves both.
Can reverse geocoding be done entirely offline?
For administrative areas, yes — a point-in-polygon lookup against boundary data answers country, region and locality with no external call. Street and rooftop precision needs an address point dataset, which is available for some markets and not for others.
Related
- Reverse Geocoding GPS Pings at Scale in Python — the concurrent dispatcher and its cache integration.
- Snapping Coordinates to the Nearest Address Point — local snapping against an address-point dataset.
- Deduplicating Driver Position Pings Before Reverse Geocoding — movement and time compression in detail.
- Geohash Encoding for Address Proximity Search — the encoder this workflow’s cache keys use.
- Detecting Geocoding Outliers With Haversine Distance — the distance check applied to forward results.