Nominatim vs Pelias vs the Census geocoder for parcel work

Geocoder comparisons usually rank accuracy, and for parcel work that is the least useful axis — because the county’s own address-point layer beats all of them, and the geocoder’s job here is to generate and confirm candidates rather than to be authoritative. What actually decides the choice is more mundane: does the service tell you how precise each result is, does it cover rural addresses, can you run it against a bulk batch without violating its terms, and can you archive its responses. This guide compares the three on those properties. It is the sourcing decision behind address geocoding & parcel matching.

Diagnosis: the four properties that actually matter jump to heading

Does it report a result kind? This is the single most important property and the one most often ignored. A service that returns a coordinate without saying whether it is a rooftop, a street interpolation or a place centroid cannot be used safely, because the pipeline has no way to distinguish a five-metre answer from a five-kilometre one. Everything in validating geocode results against parcel centroids depends on it.

The four properties, scored for parcel work A four-row by three-column matrix of the properties that decide a geocoder choice for parcel matching, scored across Nominatim, Pelias and the US Census geocoder. Rows are whether the service reports a result kind, whether a bulk batch is possible within its terms, what the licence permits for derived output, and whether responses can be archived for replay. Pelias reports match type explicitly. The Census geocoder has a true bulk endpoint and public-domain output. Nominatim can be self-hosted, which resolves both its batch and archiving constraints at once. The four properties, scored for parcel work Nominatim Pelias US Census Reports a result kind inferred from class/type match_type field implicit — always interpolated Bulk batch within terms self-host and loop self-host and loop 10 000-row files Licence on derived output ODbL share-alike depends on sources loaded public domain Responses may be archived yes, self-hosted yes, self-hosted yes strong workable with care blocking
No column wins outright, and the two rows that most often decide it are not technical: whether the licence permits publishing derived output, and whether responses may be archived. A service you cannot archive cannot support an audited match, whatever its accuracy.

Can you send a batch? Matching 14 000 permits one request at a time against a rate-limited public endpoint takes hours and is usually against the terms of use. A batch endpoint or a self-hosted instance changes the shape of the pipeline.

What are the licence terms on the output? Some licences require attribution, some restrict redistribution of derived data, and a permit-to-parcel table is derived data. This constrains what you can publish, not just what you can compute.

Can you archive and replay? A match made in March must be reproducible in September, which means storing the raw response. A service whose terms forbid caching results makes that impossible and effectively rules itself out for audited work.

Step-by-step implementation jump to heading

1. Compare them on those properties, not on accuracy claims jump to heading

Property Nominatim (OSM) Pelias US Census Geocoder
Result-kind field osm_type + class/type; precision inferred match_type (exact/interpolated/fallback), layer tigerLineId + side; interpolation is implicit
Batch endpoint none (self-host and loop) none built in (self-host and loop) yes — up to 10 000 rows per file
Self-hostable yes, well-trodden yes, designed for it no
Underlying data OpenStreetMap configurable (OSM, OA, WOF, custom) TIGER/Line
Rural US address coverage patchy — depends on local OSM contributors as good as the sources you load strong, since TIGER is the address frame
Address-point support only where OSM has them yes, if you load an address-point source no — line interpolation only
Licence on output ODbL (share-alike concerns for derived data) depends on loaded sources US public domain
Public endpoint suitable for bulk no (usage policy) depends on operator yes, within documented limits

The row that most often decides it in practice is the last-but-one. ODbL’s share-alike provisions make some organisations uncomfortable about a published parcel-match table derived from Nominatim, whereas TIGER-derived results carry no such constraint — a legal property rather than a technical one, and one that a benchmark will never surface.

2. Normalise the result kind at the boundary jump to heading

Whichever you choose, translate its vocabulary into your own immediately, so downstream code never branches on a provider’s field names.

from dataclasses import dataclass


@dataclass(frozen=True)
class Geocode:
    x: float
    y: float
    kind: str            # 'address_point' | 'rooftop' | 'interpolated' | 'place'
    provider: str
    raw: dict            # archived verbatim for replay


CONFIRMING = {"address_point", "rooftop"}


def from_census(resp) -> Geocode | None:
    """TIGER matches are line interpolations by construction. Labelling them
    'interpolated' is not pessimism — it is what they are, and it keeps them from
    confirming a match on their own."""
    m = (resp.get("addressMatches") or [None])[0]
    if not m:
        return None
    c = m["coordinates"]
    return Geocode(c["x"], c["y"], "interpolated", "census", raw=m)


def from_pelias(resp) -> Geocode | None:
    feats = resp.get("features") or []
    if not feats:
        return None
    f = feats[0]
    props = f["properties"]
    kind = {"exact": "address_point",
            "interpolated": "interpolated",
            "fallback": "place"}.get(props.get("match_type"), "place")
    if props.get("layer") not in ("address", "venue") and kind == "address_point":
        kind = "interpolated"          # an exact match on a street is still a street
    lon, lat = f["geometry"]["coordinates"]
    return Geocode(lon, lat, kind, "pelias", raw=f)


def from_nominatim(resp) -> Geocode | None:
    if not resp:
        return None
    r = resp[0]
    # Nominatim does not state interpolation directly; infer from the class/type
    # and the presence of a house number in the returned address.
    has_number = bool((r.get("address") or {}).get("house_number"))
    kind = "rooftop" if has_number and r.get("osm_type") in ("way", "relation") \
        else "interpolated" if has_number else "place"
    return Geocode(float(r["lon"]), float(r["lat"]), kind, "nominatim", raw=r)

Marking every Census result as interpolated looks harsh and is correct: TIGER geocoding places an address proportionally along a street segment, so it is exactly the result kind that must never confirm a match alone. It is still an excellent candidate generator, and for rural US addresses it is often the only one with coverage.

3. Run them as a ladder, not a choice jump to heading

The properties are complementary, so the practical answer is usually more than one.

Where 5 000 addresses resolve on a three-rung ladder Stacked bar chart of where addresses resolve on a three-rung geocoding ladder, for an urban and a rural file of 5 000 records each. In the urban file the county address-point layer resolves 4 410, Pelias a further 380, the Census geocoder 140, and 70 fail. In the rural file the address-point layer resolves only 1 980, Pelias 1 240, and the Census geocoder 1 460 — its TIGER frame covers rural addresses the other rungs miss — with 320 failing. Where 5 000 addresses resolve on a three-rung ladder 0 2000 4000 6000 4410 urban file 1460 1240 1980 rural file Addresses (5 000 each) no result rung 3 — Census (TIGER) rung 2 — Pelias rung 1 — county address points Rung 3 results are interpolated by construction and never confirm a match alone.
The ladder short-circuits: in the urban file 88% resolve on the first rung and the lower rungs are barely called. Rurally the picture inverts and the Census rung carries 29%, which is the argument for keeping it in the ladder rather than choosing one provider outright.
def geocode(address, providers, archive) -> Geocode | None:
    """Try in order of the precision they can attest to. Stop at the first result
    that can CONFIRM; keep the best candidate otherwise."""
    best = None
    for p in providers:                       # e.g. [local_address_points, pelias, census]
        try:
            g = p.geocode(address)
        except TransientError:
            continue
        if g is None:
            continue
        archive.put(address, p.name, g.raw)    # every response, for replay
        if g.kind in CONFIRMING:
            return g
        best = best or g
    return best

4. Self-host when the batch size justifies it jump to heading

Fourteen thousand addresses against a public endpoint is a poor citizen and a slow pipeline. Both Nominatim and Pelias are designed to be self-hosted; a single instance loaded with your state’s extract answers a batch in minutes and removes the rate-limit question entirely. The Census geocoder cannot be self-hosted, but its bulk file endpoint accepts ten thousand rows at a time, which serves the same purpose.

Verification & testing jump to heading

def test_census_results_never_confirm():
    g = from_census(census_fixture("1240 County Road 27"))
    assert g.kind == "interpolated"
    assert g.kind not in CONFIRMING


def test_pelias_street_match_is_not_an_address_point():
    g = from_pelias(pelias_fixture(match_type="exact", layer="street"))
    assert g.kind == "interpolated"       # exact on a STREET is still not a point


def test_every_provider_result_is_archived(archive):
    geocode("123 N Main St", [pelias, census], archive)
    assert archive.count("123 N Main St") >= 1


def test_ladder_stops_at_the_first_confirming_result(archive):
    g = geocode("123 N Main St", [address_points, pelias, census], archive)
    assert g.provider == "address_points"
    assert not pelias.called

Before committing to a provider, run a coverage probe: take 500 addresses stratified between urban and rural parts of your counties, geocode them, and tabulate result kinds by provider. That is a measurement about your own data, which is the only comparison that generalises to your pipeline.

What each provider can and cannot attest A three-row by four-column matrix of providers against the result kinds each can legitimately report. Rows are Nominatim, Pelias and the Census geocoder. Columns are address point, rooftop, street interpolation and place fallback. Pelias can report all four when an address-point source is loaded. Nominatim reports rooftops where OpenStreetMap has building geometry with a house number. The Census geocoder can only ever report an interpolation, because TIGER geocoding places addresses proportionally along street segments. What each provider can and cannot attest address point (confirms) rooftop (confirms) street interpolation place fallback Nominatim (OSM) where OSM has them yes yes yes Pelias (sources loaded) yes yes yes yes US Census (TIGER) never never always ZIP centroid can report sometimes cannot
Only the first two columns can confirm a match. The Census row has no cell in either of them, which is not a criticism of the service — it is a statement of what TIGER geocoding does, and it is why the normaliser labels every Census result interpolated regardless of how confident the response looks.

Failure recovery jump to heading

A provider’s terms turn out to forbid caching. The archive is a hard requirement for audited matching, so this rules the provider out for that use. Re-derive the affected matches with a provider you can archive, and keep the original as a candidate generator only.

Coverage worse than expected in rural counties. Add the Census geocoder to the ladder as a fallback rather than switching wholesale. Its TIGER frame is strongest exactly where OSM-based coverage is thinnest, and the ladder makes adding it a one-line change.

A self-hosted instance drifts from the public one. Pin the extract date and record it alongside the archived response, so a match made against a March extract is reproducible even after the instance is rebuilt. Without that, the archive proves what was returned but not what the index contained.

Frequently asked questions jump to heading

Which one is most accurate?

The wrong question for this work, because the county’s own address-point layer beats all three and should be tried first. What separates them is whether they tell you how precise each result is, how they cover rural addresses, and what their licence permits you to publish. A service that returns a slightly better coordinate without a result kind is less useful than one that returns a worse coordinate honestly labelled.

Why treat every Census result as interpolated?

Because that is what TIGER geocoding does: it places an address proportionally along a street segment. The coordinate is not a building, and on blocks with uneven lot widths it lands on the wrong parcel routinely. It remains an excellent candidate generator, particularly for rural addresses where its coverage is the best available — it just cannot be the evidence that confirms a match.

Does the licence really matter if we only use results internally?

It matters as soon as a derived table leaves the building — a published dataset, a report to a client, an API. A permit-to-parcel table built with a share-alike geocoder is derived data, and the obligations may attach to it. This is worth resolving before the pipeline is built rather than after somebody asks to publish the output.

Is running two geocoders wasteful?

No, because the ladder short-circuits: the second is only called when the first returns nothing or returns a result too imprecise to confirm. In practice most addresses resolve at the first rung, and the fallback earns its place on exactly the rural residue that would otherwise be unmatched.