Geocoding & Address Normalization

Every coordinate in a retail location pipeline began life as a string somebody typed, and the quality of that translation sets a ceiling on everything downstream. This section covers the stage that turns addresses into points: parsing them into components, standardizing them against a reference, geocoding them at a known precision, and proving the result is good enough to route from.

It is the least glamorous stage in the stack and the one that quietly decides the most. A store geocoded to the centre of its postcode rather than to its door moves by a few hundred metres, which is invisible on a national map and enough to put its drive-time catchment on the wrong side of a dual carriageway. A customer address that failed to parse is silently dropped from a demographic join, and the market it came from looks weaker than it is. Neither failure raises an exception, and both survive review, which is why this stage needs gates rather than good intentions.

Concept and Theory: Geocoding Is Two Problems, Not One

The word “geocoding” hides a parsing problem inside a matching problem, and they fail differently.

Parsing takes a single string and decomposes it into typed components: house number, street name, street type, unit designator, place, region, postal code. It is a language problem. The same physical address arrives as 1200 N Wells St Ste 4, 1200 North Wells Street, Suite 4 and Suite 4, 1200 N. Wells, and a parser has to recognise all three as the same components in different orders with different abbreviations. Parsers fail on ambiguity — Saint versus St versus a street named St James — and their failures are recoverable, because a partially parsed address can still be matched.

Matching takes those components and finds the corresponding record in a reference dataset: a parcel, a building, an address point, a street segment. It is a search problem with a scoring function, and its failures are quieter. A match against a street segment with linear interpolation always succeeds and always returns a point — a point that may sit fifty metres from the building or, on a long rural segment, several hundred. Nothing about that result announces itself as weaker than a rooftop match unless the geocoder reports its precision and the pipeline reads it.

Parsing and matching are separate stages with separate failure modes A raw address string is parsed into typed components, then matched against a reference dataset to produce a coordinate and a precision tier. Parsing failures are loud and recoverable; matching failures are quiet, because interpolating along a street segment always returns a plausible point. One word, two problems, two kinds of silence raw string 1200 N Wells St Ste 4 CHICAGO IL 60610 parse into components number · predirectional · name type · unit · place · region · zip match to a reference coordinate + precision tier + the record it matched parsing fails loudly an unrecognised component raises, or the confidence drops below a threshold → the record lands in a queue someone works matching fails quietly interpolation along a street always returns a coordinate, on a rural segment 400 m out → nothing stops it unless you read the tier The practical consequence: a pipeline can measure its parse rate easily and its match quality only by recording the precision tier of every point and treating the weak tiers as a distinct population.

Precision Tiers: The Field That Makes the Rest Possible

Every serious geocoder returns some indication of how the point was derived. The names differ, the concept does not, and the tiers form a hierarchy of ground accuracy that a pipeline can act on.

Tier How the point is derived Typical error Safe to use for
Rooftop / building Matched to a building or address point 0–15 m Everything, including walk catchments
Parcel centroid Centre of the matched land parcel 10–60 m Drive-time catchments, competitor distances
Street interpolation Position along a street segment by house number 20–400 m Drive-time catchments in urban areas only
Postal centroid Centre of the postal code area 100 m–5 km Market-level aggregates, nothing site-specific
Place / locality Centre of a named place 1–20 km Nothing — quarantine it

The rule that makes this table operational is that the tier travels with the coordinate for the rest of its life. A point stored without its tier is a point whose reliability has to be guessed at, and the guess is always optimistic. Once the tier is a column, the validation rules for store coordinates can scale their tolerances to it — a rooftop point disagreeing with its address by 400 metres is a real error, while a postal-centroid point doing the same is behaving exactly as advertised.

Where the weak points actually are Across 4,610 store records, 78 per cent geocode to rooftop, 14 to parcel, 6 to street interpolation and 2 to postal centroid. Across 1.9 million customer addresses the same geocoder returns 41 per cent rooftop and 22 per cent postal centroid, because customer-entered addresses are far messier than a curated store list. The same geocoder, two very different populations store estate · 4,610 curated records rooftop 78% parcel 14% street 6% · postal 2% customer file · 1.9M entered addresses rooftop 41% parcel 24% street 13% postal 22% Twenty-two per cent of customer records at postal-centroid precision is not a geocoder problem — it is what happens when people type their own addresses. It is also 418,000 records that must not be used to measure anything at street scale, however tempting the sample size is. Tiers as reported by the geocoder; no reclassification applied.

Architecture: Where Geocoding Sits in the Pipeline

Geocoding belongs at the boundary, immediately after ingestion and before anything spatial happens. Placing it later — inside the scoring run, say, or on demand when a map is drawn — produces two problems that are hard to undo: the same address gets geocoded repeatedly with results that may differ as the reference data updates, and the coordinate acquires no provenance, because nobody recorded which reference version produced it.

The shape that works is a normalization service with a persistent result store, keyed on the normalized address rather than on the raw string. Ingestion writes raw records; the normalizer parses and standardizes them; the geocoder resolves anything not already in the store; and downstream stages read coordinates that are already tiered, dated and attributable. This is the same decoupling principle applied throughout the architecture foundations, and it pays off in the same way: the expensive stage runs once, its output is versioned, and a change to a downstream consumer never triggers a re-geocode.

python
import hashlib
from dataclasses import dataclass

import usaddress


@dataclass(frozen=True)
class NormalizedAddress:
    number: str
    street: str
    unit: str
    city: str
    region: str
    postcode: str

    def key(self) -> str:
        """Stable key for the result store — normalization must be deterministic."""
        raw = "|".join([self.number, self.street, self.unit,
                        self.city, self.region, self.postcode]).upper()
        return hashlib.sha256(raw.encode()).hexdigest()[:20]


ABBREV = {"STREET": "ST", "AVENUE": "AVE", "ROAD": "RD", "NORTH": "N",
          "SOUTH": "S", "EAST": "E", "WEST": "W", "SUITE": "STE"}


def normalize(raw: str) -> NormalizedAddress:
    """Parse a free-text address into typed, standardized components."""
    parsed, _kind = usaddress.tag(raw)          # raises on genuinely ambiguous input
    def part(*keys: str) -> str:
        vals = [parsed.get(k, "") for k in keys]
        joined = " ".join(v for v in vals if v).upper().replace(".", "")
        return " ".join(ABBREV.get(tok, tok) for tok in joined.split())

    return NormalizedAddress(
        number=part("AddressNumber"),
        street=part("StreetNamePreDirectional", "StreetName", "StreetNamePostType"),
        unit=part("OccupancyType", "OccupancyIdentifier"),
        city=part("PlaceName"),
        region=part("StateName"),
        postcode=part("ZipCode")[:5],
    )

Two details in that function carry more weight than they look. The key is built from the normalized components rather than the raw string, so three spellings of one address share a cache entry and a coordinate — which is what makes the result store useful rather than merely large. And the abbreviation map is applied consistently in one direction; a pipeline that expands abbreviations in one place and contracts them in another will produce two keys for one address and never notice.

Configuration Parameters

Parameter Typical value Notes
min_parse_confidence 0.85 Below this the record goes to the review queue, not the geocoder
accepted_tiers rooftop, parcel Tiers usable for site-level analysis; others are flagged
interpolation_max_segment_m 400 Reject street matches on segments longer than this
batch_size 500 Requests per call to a batch endpoint
max_qps 10 Client-side rate limit, below the provider’s ceiling
reference_version dated Recorded with every result; changing it invalidates the store
dedupe_distance_m 25 Two records closer than this with similar names are candidates for merging
retry_backoff_s 1, 4, 16 Transient failures only; a parse failure is never retried

The parameter that causes the most argument is accepted_tiers, and the argument is usually a symptom of the wrong question. The tiers are not a quality bar to be raised until enough records pass — they are a description of what each point can support. Street-interpolated points are entirely adequate for a market-level demand estimate and inadequate for measuring the distance from a candidate site to its nearest competitor, so the right answer is per use, recorded per record, rather than a single threshold applied everywhere.

Deduplication: The Problem Normalization Exposes

Normalizing addresses makes duplicates visible that were previously hidden behind spelling differences, and duplicates matter more in a retail pipeline than in most: a store counted twice inflates a market’s coverage, doubles a competitor’s apparent presence, and creates a phantom cannibalization signal against itself.

The usual duplicates are not exact matches. They are the same location expressed as a suite and a unit, as two names for one plaza, or as a delivery entrance and a customer entrance thirty metres apart. Catching those requires combining a normalized-string comparison with a spatial one, which is why deduplication belongs after geocoding rather than before it.

Four kinds of duplicate, four detections Exact duplicates after normalization are caught by the key; unit-level variants by comparing without the unit; two entrances of one site by a distance test under twenty-five metres; and a renamed plaza by fuzzy name matching within a shared parcel. Only the first is caught by string equality alone. String equality catches the easy quarter duplicate type what detects it share same address, different spelling the normalized key 26% suite 4 versus unit 4 key without the unit component 31% two entrances, one site distance under 25 m + name similarity 28% plaza renamed after a sale shared parcel + fuzzy name match 15% Three quarters of duplicates need geometry as well as text, which settles the ordering question: normalize, geocode, then deduplicate — never the other way round. Merge into a surviving record with the others kept as aliases. A hard delete removes the evidence that the merge happened, and the next import recreates the duplicate.

Scaling and Cost: Geocoding Is a Budget Line

At a few thousand store records, geocoding is free in every sense — a single batch, a few seconds, no meaningful cost. At a few million customer addresses it becomes a line item, and the shape of the spend is worth understanding before it arrives.

Three factors dominate. Volume of distinct addresses, not raw records, is what a well-designed pipeline pays for, because the result store answers everything it has seen before; a customer file of two million rows routinely contains fewer than a million distinct normalized addresses, and a delivery file far fewer still. Refresh policy decides how often that number is paid again: tying re-geocoding to reference-data releases rather than to a schedule turns an annual cost into an occasional one. And precision requirements decide which provider is needed at all — a market-level aggregate can be served by a free postal-code lookup, while a walk-catchment analysis needs rooftop matches that only a real geocoder provides.

The architecture that keeps the bill proportionate is the one already described: normalize first so the cache key is stable, store every result with its reference version, and let the pipeline request only what is genuinely new. Teams that skip the store typically discover the problem when a routine backfill re-geocodes the entire history, and the discovery arrives as an invoice rather than as an alert.

Throughput matters as much as cost. A provider that accepts batches of five hundred addresses per call will complete a million-address backfill in a couple of thousand calls; the same work issued one address at a time is a million round trips and, at any sensible client-side rate limit, a job measured in days. Batching is therefore not an optimisation but the difference between a backfill that fits in a maintenance window and one that does not — and the same applies to a self-hosted service, where the per-request overhead is smaller but the concurrency ceiling is real.

Validation and QA Gates

The gates that matter here are cheap and specific, and they run on every ingest:

  • Parse rate — the share of records that produced a full component set, tracked per source. A drop is a source-format change, not a data problem.
  • Tier distribution — the share of results in each precision tier, compared against the previous run. A sudden shift toward postal centroids means the reference data or the matcher changed.
  • Jurisdiction agreement — the geocoded point falls inside the administrative area its address claims. This single check catches transposed coordinates, wrong-state matches and a surprising number of genuine data-entry errors.
  • Displacement on re-geocode — when a record is geocoded again under a new reference version, the distance it moved. Most should move less than a metre; the tail is where the reference improved or regressed.
  • Duplicate rate — new duplicates found per import, by type. A rising unit-variant rate usually means a source system started emitting a field it previously omitted.

None of these needs a model, and all of them are the kind of measurement that turns an invisible degradation into a number on a dashboard. Feeding them into the same drift monitoring that watches the demographic layers keeps the whole ingestion boundary under one set of eyes.

Integration Notes: What the Next Stage Expects

Downstream, the spatial database stores the coordinate with its tier, its reference version and the normalized key that produced it. The isochrone stage snaps that coordinate to the road network and will happily snap a postal-centroid point to a motorway; passing the tier through lets the routing stage refuse, or at least flag, contours built from points that were never precise enough to route from. And the competitor proximity index measures distances between points whose own uncertainty may exceed the differences being measured — which is only knowable if the tier travelled with them.

Frequently Asked Questions

Should we build a geocoder or use a service?

Use a service until reproducibility or volume forces the question, then self-host. A hosted geocoder is accurate, maintained and immediately available, and its weakness is that it improves underneath you: the same address geocoded a year apart can return different coordinates, which is fine for a delivery and awkward for an audited site-selection model. Self-hosting an open stack costs an import pipeline and some memory, and buys a pinned reference version you can cite. Many teams end up with both — the service for coverage and the self-hosted instance for anything that has to be reproduced.

What should happen to an address that will not geocode?

It goes to a review queue with its parse output attached, and the record it belongs to is marked incomplete rather than dropped. Two things follow from that. The pipeline can report how much of a market is unresolved instead of quietly under-counting it, and the queue itself becomes a measure of source quality — a source contributing most of the failures is a conversation with the team that owns it, not an endless manual cleanup task.

How often should addresses be re-geocoded?

When the reference data updates, and not otherwise. Re-geocoding on a schedule wastes requests and, worse, introduces movement in coordinates for no reason a reader can point to. Tying the re-run to a new reference version means every movement has an explanation, and comparing the before-and-after displacement gives a direct measure of what the new reference actually changed — usually a small tail of large improvements rather than a general drift.

Is postal-code centroid geocoding ever acceptable?

For market-level aggregates, yes; for anything site-specific, no. A postal centroid is a legitimate answer to “which market is this customer in” and a meaningless answer to “how far is this customer from the store”, and the same coordinate can be both depending on what it is asked. Keeping the tier on the record is what allows one dataset to serve both purposes honestly, with the site-level analysis filtering to the tiers that can support it.

How do international addresses change the picture?

They change the parsing far more than the matching. Address components, their order and their conventions differ by country, so a parser tuned for one country produces confident nonsense on another — which is worse than failing. Where a pipeline spans countries, route each record to a country-specific parser on the strength of an explicit country field rather than inferring it, keep the normalized schema common, and expect the tier distribution to vary substantially between markets for reasons that have nothing to do with your data.

Conclusion

Geocoding is where a text problem becomes a spatial one, and everything after it inherits whatever error was introduced here. Treat it as a distinct pipeline stage with its own store, keep the precision tier attached to every coordinate, deduplicate after geocoding rather than before, and gate each import on parse rate, tier distribution and jurisdiction agreement. Do that and the coordinates that reach the routing and scoring stages come with the one thing they most need: a documented claim about how much they can be trusted.

← Back to Location Intelligence Architecture & Data Foundations