Rooftop, Interpolated or Centroid: Geocoding Precision Levels
Problem statement
Two rows in a geocoded table look identical:
customer lat lon
A 51.503488 -0.127697
B 51.507446 -0.127765
One is the front door of a specific building. The other is the centre of Greater London, standing in for an address the geocoder could not find. Both are valid WGS84 coordinates with six decimal places. Nothing in the numbers distinguishes them.
The difference is the precision level: what kind of feature the geocoder actually matched. It is reported in every serious geocoding response, discarded by most pipelines, and it is the single most important field for anything downstream that measures distance.
Quick answer
There are six levels that matter, and they differ by orders of magnitude. Measured on one real address, taking the matched feature's bounding-box diagonal as the size of the answer:
level example match feature span error vs the door
rooftop the building itself 55 m 0 m
street the road it stands on 97 m 50 m
postcode the postal unit 1,975 m 0 m
locality "London" 73,804 m 440 m
region a state or county varies varies
country "United Kingdom" 1,615,489 m 413,077 m
Keep the level with the coordinate, and set a floor per analysis:
PRECISION_RANK = {"rooftop": 0, "building": 0, "house": 0, "office": 0,
"street": 1, "road": 1,
"postcode": 2, "postal": 2,
"locality": 3, "suburb": 3, "city": 3, "town": 3,
"region": 4, "state": 4, "county": 4,
"country": 5}
FLOOR = {
"delivery routing": "rooftop",
"walking catchment": "street",
"site selection": "street",
"postcode-level report": "postcode",
"national choropleth": "locality",
}
Step-by-step solution
1. Learn what each level physically is
Rooftop / address point. A coordinate stored for that specific address, usually from a national address register or an OSM address node. Error: metres. It is a point on or near the building, not necessarily the entrance.
Parcel or building centroid. The middle of the property. Error: tens of metres, more for large sites. Note that a centroid can fall outside a U-shaped building or on the wrong side of a courtyard.
Street interpolation. The geocoder knows the road runs from number 1 to number 99 and places number 50 halfway along. Error: tens of metres in a dense terrace, hundreds of metres on a rural road where numbering is irregular, and systematically biased where houses are not evenly spaced.
Postcode / ZIP centroid. A representative point for a postal unit. In the UK a postcode covers roughly fifteen addresses and the measured span was 1,975 m; a US ZIP covers thousands of addresses and is not officially a polygon at all.
Locality centroid. The middle of a town or city. The measured "London" match had a bounding box spanning 73.8 km. It was still only 440 m from the true address, because that address happens to be near the centre โ which is exactly how this level fools people.
Region and country centroid. A single point standing for an administrative area. The measured country match was 413 km from the address.
2. Map the provider's vocabulary to your own ladder
Every provider names these differently: addresstype in Nominatim, location_type with values like ROOFTOP and GEOMETRIC_CENTER elsewhere, match_level, accuracy, or a set of match codes. Normalise them into one internal vocabulary at the adapter, exactly as you would normalise units.
Do this even with a single provider. It gives you one place to fix the mapping when the provider adds a level, and it makes multi-provider tables comparable.
3. Set the floor from the analysis, not from the data
The right floor is a property of the question:
- Point-in-polygon into small areas (census output areas, delivery zones): rooftop or street. A postcode centroid crosses small-area boundaries routinely.
- Distance to nearest facility: street, if the facilities are kilometres apart; rooftop if they are hundreds of metres apart.
- Isochrones and routing: street at minimum โ the router needs to snap to the network anyway.
- National or regional summaries: postcode or locality is genuinely adequate, and pretending otherwise wastes money.
Write the floor down in the pipeline config next to the analysis it belongs to. It is a parameter of the analysis, not a global setting.
4. Decide the fallback policy explicitly
When a row fails to meet the floor, there are exactly three honest options:
- Drop it, and report how many were dropped and why.
- Keep it at the coarser level, labelled, and exclude it from the analyses that cannot tolerate it.
- Send it for review.
The dishonest fourth option is to keep it unlabelled, which is what happens by default when the precision column is not stored.
5. Make the level visible on the map
A map of geocoded points should show precision, not hide it. Symbolise by level โ solid for rooftop, hollow for street, cross-hatched for postcode โ or draw the uncertainty as a circle whose radius is the typical error for that level.
The first time a stakeholder sees the 2 km circles around a third of the points, the conversation about data quality happens on its own.
Code examples
Example 1 โ normalising provider vocabularies
NOMINATIM_LEVELS = {
"house": "rooftop", "building": "rooftop", "office": "rooftop",
"amenity": "rooftop", "shop": "rooftop", "place_of_worship": "rooftop",
"road": "street", "residential": "street", "footway": "street",
"postcode": "postcode",
"suburb": "locality", "neighbourhood": "locality", "village": "locality",
"town": "locality", "city": "locality", "municipality": "locality",
"county": "region", "state": "region", "province": "region",
"country": "country",
}
TYPICAL_ERROR_M = { # order-of-magnitude, for uncertainty circles
"rooftop": 10, "street": 60, "postcode": 500,
"locality": 5_000, "region": 50_000, "country": 400_000,
}
def to_level(provider: str, raw_level: str | None) -> str:
if provider == "nominatim":
return NOMINATIM_LEVELS.get(raw_level, "unknown")
raise ValueError(f"no level mapping for provider {provider!r}")
The TYPICAL_ERROR_M table is deliberately coarse. Its job is to make the uncertainty visible on a map, not to be a rigorous error model โ and the values come from the measured ladder above rather than from optimism.
Example 2 โ a precision audit of a geocoded file
import pandas as pd
def precision_audit(df: pd.DataFrame, level_col="precision") -> pd.DataFrame:
counts = df[level_col].value_counts(dropna=False)
order = ["rooftop", "street", "postcode", "locality", "region", "country", "unknown"]
table = pd.DataFrame({
"rows": [counts.get(level, 0) for level in order],
}, index=order)
table["pct"] = (100 * table["rows"] / len(df)).round(1)
table["typical_error_m"] = [TYPICAL_ERROR_M.get(level) for level in order]
table["cumulative_pct"] = table["pct"].cumsum().round(1)
return table
rows pct typical_error_m cumulative_pct
rooftop 6,241 62.4 10 62.4
street 2,118 21.2 60 83.6
postcode 1,004 10.0 500 93.6
locality 512 5.1 5,000 98.7
region 0 0.0 50,000 98.7
country 125 1.3 400,000 100.0
unknown 0 0.0 None 100.0
The cumulative_pct column is the one to quote: "83.6% of this file is street level or better" is a sentence a stakeholder can act on.
Example 3 โ drawing the uncertainty instead of hiding it
import geopandas as gpd
def uncertainty_circles(points: gpd.GeoDataFrame, level_col="precision",
projected_crs=3857) -> gpd.GeoDataFrame:
"""One buffer per point, sized by the precision level it was matched at."""
out = points.to_crs(projected_crs).copy()
out["radius_m"] = out[level_col].map(TYPICAL_ERROR_M).fillna(1_000)
out["geometry"] = out.buffer(out["radius_m"])
return out
def plot_by_precision(points, ax, level_col="precision"):
styles = {
"rooftop": dict(marker="o", markersize=18, color="#14b8a6"),
"street": dict(marker="o", markersize=14, color="#0ea5e9"),
"postcode": dict(marker="s", markersize=12, color="#d97706"),
"locality": dict(marker="^", markersize=14, color="#ef4444"),
"country": dict(marker="X", markersize=22, color="#ef4444"),
}
for level, style in styles.items():
subset = points[points[level_col] == level]
if len(subset):
subset.plot(ax=ax, label=f"{level} ({len(subset)})", **style)
ax.legend(title="match precision", fontsize=8)
Use a projected CRS for the buffers. Buffering degrees produces ellipses that shrink towards the poles, which is a different kind of wrong answer about uncertainty.
Explanation
Why the postcode level is the most misleading
Postcode geocodes feel precise because postcodes are precise-looking codes. The precision of the code has nothing to do with the precision of the coordinate.
A UK postcode covers roughly fifteen addresses, and the measured postal unit spanned 1,975 m โ because postcodes follow delivery routes, not compact shapes. A US ZIP code covers thousands of addresses across an area that can exceed a hundred square kilometres, and ZIP "boundaries" are reconstructions rather than official geography.
The practical rule: a postcode centroid is a fine unit of analysis for a report about postcodes, and a poor location for anything about individual addresses.
Why the locality level fools everyone at least once
The measured "London" match sat 440 m from the true address. That looks acceptable โ until you notice the feature it matched spans 73.8 km.
The 440 m was luck: the address is near the city centre, and the centroid is near the city centre. A different address, out in the suburbs, would have inherited the same coordinate and been 20 km wrong. This is why per-row error checks against a sample can pass while the level is systematically unusable: the errors depend on where in the locality each address actually is.
Why street interpolation is biased rather than noisy
Interpolation assumes house numbers are evenly spaced along the segment. Real streets are not: a terrace of small houses at one end and a school at the other pushes every interpolated number towards the terrace.
The error is therefore correlated within a street, not independent per address. Averaging many interpolated points does not cancel it out, which matters when you aggregate them into small areas.
Why the level must travel with the coordinate
Once the precision column is dropped, no downstream process can recover it. The coordinate is complete and plausible; there is no test that distinguishes a rooftop match from a locality centroid except stacking โ many rows sharing one coordinate โ and that only catches the extreme cases.
Two columns, precision and provider, cost a few bytes per row and are the difference between a dataset you can audit and a dataset you have to re-geocode.
Edge cases or notes
- A centroid can be outside its own polygon โ a crescent-shaped locality or a country like Chile. Use a point-on-surface if you must generate one yourself.
- Rooftop does not mean the entrance. For routing, the access point can be a hundred metres from the address point.
- Interpolated ranges can be reversed, putting odd numbers on the wrong side of the street.
- Large sites โ campuses, industrial estates, airports โ return one point for hundreds of doors, at "rooftop" precision.
- Flats and units usually share the building's coordinate; the vertical dimension does not exist in the reference data.
- PO boxes geocode to the sorting office at rooftop precision. The level is honest; the meaning is not.
- Stacked coordinates are the fingerprint of a fallback. Count duplicate coordinates as a routine check.
- Precision is not accuracy. A rooftop match against a badly digitised register is precise and wrong.
Internal links
- Geocoding explained: from an address string to a coordinate โ where the levels come from
- Match quality explained: reading a geocoder's confidence score โ the other half of the quality picture
- How to validate geocoding results before you trust them โ testing the level against geometry
- Fixing geocodes that all land in the country centroid โ the worst level, diagnosed
- How to reverse geocode points to addresses and areas โ the same ladder, in the other direction
- Coordinate precision explained โ decimal places are not accuracy either
- How to batch geocode thousands of addresses in Python โ recording the level at volume
- Spatial data quality dimensions โ positional accuracy in context
FAQ
What does "rooftop" precision mean?
The geocoder matched an address point stored for that specific property, usually from an address register. Expect an error of metres โ but it is a point on the property, not necessarily the entrance.
Is a postcode centroid good enough?
For reports about postcodes, yes. For anything about individual addresses, no: the measured postal unit spanned 1,975 m, and US ZIP codes cover thousands of addresses.
How far wrong can a locality centroid be?
Anywhere within the locality. The measured "London" match had a 73.8 km bounding box; it happened to be 440 m from the target address, and would have been tens of kilometres out for a suburban one.
Which precision do I need for a catchment analysis?
Street level at minimum, rooftop if the catchments are small. A postcode centroid crosses small-area boundaries often enough to change the answer.
How do I show precision on a map?
Symbolise by level and, where it matters, draw a buffer sized by the typical error for that level. It moves the data-quality conversation to the start of the meeting.
Can I improve the precision of an existing geocoded file?
Only by re-geocoding the coarse rows, usually against better reference data or after cleaning the address text. There is no way to refine a centroid into a rooftop after the fact.