Skip to main content

Coordinate-first resolver & falsehood-aware reconciliation — direction (2026-06-04)

We spent the PR3 cycle trying to fix German inside the parser — a self-conditioned retrain, then a decoderdecoderIn a transformer encoder-decoder model, the part that produces output sequences. Mailwoman's classifier is encoder-only (no decoder); the 'CRF decoder' is a different thing — a structured-prediction layer that picks the best label sequence from the encoder's outputs. repair — and both produced correct-er parsesaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. that the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. couldn't cash in. The resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. is the judge, and the judge kept saying the same thing: German is a resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. problem, not a parser problem. This is the plan that follows from finally believing it.

DeepSeek-signed across the Pilot-A postmortem consult (.agents/skills/deepseek-consult/session-notes-2026-06-04-pilot-a-postmortem.md, …-resolver-coordinate-first.md), grounded in how PeliasPeliasAn open-source geocoder, Mailwoman's spiritual predecessor. and Airmail actually work.

Why now

PR3 Pilot A (self-conditioningself-conditioningAn auxiliary model head that predicts the input's locale (language/country) and then FiLM-modulates the encoder so parsing adapts to the detected locale. Structural facts like word order still need explicit training on both orders., from scratch US/FR/DE) came back a clean negative: DE localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-match 25.6% vs v0.7.2's 77.4%, the v0.8.0 spanspanA contiguous range of characters or tokens in the input string, tagged with an address component type (street, locality, postcode, etc.). Parsed addresses are represented as collections of spans, possibly nested in a tree. collapse reproduced via a new mechanism, on correct data, and converged (flat 10k→20k). The decoderdecoderIn a transformer encoder-decoder model, the part that produces output sequences. Mailwoman's classifier is encoder-only (no decoder); the 'CRF decoder' is a different thing — a structured-prediction layer that picks the best label sequence from the encoder's outputs. follow-up — the postcode-trim, which hands a swallowed citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy.-start back to the citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. — fixed the parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. (auen VogtlPlauen Vogtl, +36pp DE exact-localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy.) and moved the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. zero.

That zero is the whole story. The parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. was string-perfect and the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. still missed, because our resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. matches the parsed localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. string against WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. names, and OA's gold carries a regionregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. suffix WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. doesn't store (Plauen Vogtl vs WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations.'s Plauen). Even v0.7.2, which parsesaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. German cleanly, only resolves 54.7% of Saxony for this reason. We were optimising a name-equality the gazetteergazetteerA geographical index that maps place names and postcodes to real-world coordinates. Mailwoman uses a custom-built Who's On First (WOF) SQLite database as its gazetteer — the 'atlas' half of the grammar/atlas architecture.'s canonical names can't satisfy.

The reframe: the German "wall" is largely a metric artifact

Our oa-resolver-eval localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy.-match is name-centric — it compares the resolved WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. name to OA's gold name string. It penalises a geographically-correct resolve that returns Plauen against gold Plauen Vogtl. Meanwhile the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. already places these German addresses at coord p50 1.3 km via the postcode anchoranchor inferenceA technique where structured knowledge (postcode locations, gazetteer place names) is injected into the model as soft input features — not as deterministic overrides. The model still decides the final labels, but the anchor signal biases it toward correct admin tags.. So a large slice of the "25.6% / 77.4% German localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy." number was never a geocodinggeocodingThe process of converting an address into geographic coordinates (latitude and longitude). Mailwoman geocodes in a multi-tier cascade: exact address-point match → street interpolation → locality centroid. Each tier is progressively coarser but more widely available. failure — it was a scoreboard that can't read a name variant.

The fix starts with the scoreboard. The non-gameable metric: a localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. is correctly resolved iff the real OA per-address point lies inside the polygon of the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-assigned WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. (point-in-polygon containment, not centroid distance — distance is gameable in a dense metro). The circularity guard is absolute: score against the real OA point, never the postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. centroid the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. itself consumed.

How the rest of the field does it

PeliasPeliasAn open-source geocoder, Mailwoman's spiritual predecessor. doesn't name-match for admin at all. Its wof-admin-lookup discards the source record's citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. names and regenerates the whole hierarchy from WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. polygons via point-in-polygon on the coordinate, at import — "we ignore all admin hierarchy from individual records and generate it from the polygon data." The Plauen Vogtl mismatch can't arise. Its postal-cities precomputes a postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. table that aggregates multiple names per postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. (18964 → both "Souderton" and "Franconia"). Its point-in-polygon is an in-memory rbush R-tree + ray-cast, not Elasticsearch and not SpatiaLite.

Airmail (and Nominatim, Photon) are retrieval geocoders — BM25 over indexed places. A wrong postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. just lowers a document's score; the right citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. still ranks it up. Robust, but with no explicit "these two fields disagree" signal, so they silently mislocate when a wrong combination scores high (90210 Los Angeles → Beverly Hills). That silence is the gap we can fill.

The lesson: PeliasPeliasAn open-source geocoder, Mailwoman's spiritual predecessor. trusts the coordinate because at import it has the true address point. We are a query-time parser+resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. — the postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. is the user's claim, which can be transposed, stale, or borrowed from the next town over. So we cannot blindly trust it the way PeliasPeliasAn open-source geocoder, Mailwoman's spiritual predecessor. does. The parsed citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. name is our independent cross-check, and the trim is what made it reliable.

The design

A coordinate-first resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. that scores rather than hard-matches, and flags rather than launders.

Candidate generation (coordinate-first, in-DB, no ES)

Precompute a postcode → candidate localities table offline, where the geometry lives. For each postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one., take its centroid, point-in-polygon (or nearest within ~10 km) against WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. polygons, and store the containing/adjacent localities with their canonical name and WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. alt-names. This is PeliasPeliasAn open-source geocoder, Mailwoman's spiritual predecessor.'s move keyed on postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.; the heavy geometry runs once at build time, and the shipped artifact is a flat SQLite lookup. No SpatiaLite-for-web — it's a lookup we precompute, not a spatial engine we ship.

At resolve time, the candidate set is the postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.-proximal localities ∪ a few name-matched localities (countrycountryThe top-level address component (an ISO country). Closed-vocabulary, so it is best handled by a deterministic matcher feeding a proposal rather than a retrained model head.-filtered fuzzy), typically ≤5.

Soft-scoring reconciliation (retrieval robustness + explicit conflict)

Score each candidate c:

Score(c) = 0.6·S_pc + 0.3·S_name + 0.1·S_pop
S_pc = exp(−dist/2km), or 1.0 if the postcode point is inside c's polygon
S_name = fuzzy match of the parsed locality vs c's canonical name + alt-names
S_pop = log-normalised population, a tiebreak when evidence is weak

Pick the top. Confidence = logistic(margin between the top two.) And the differentiator a BM25 geocoder structurally can't offer: raise a postcode_city_mismatch flag when |S_pc − S_name| > 0.5 — the postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. points one way, the name another. Return the best candidate, but with lowered confidence and the flag attached. Never silently overwrite a strong name match with a postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.-only guess.

Output the canonical WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. name (that's what geocodes), and echo the parsed string as parsed_locality for transparency.

Reconciliation policy (default)

  • Both S_pc and S_name > 0.6 → return, confidence ≥ 0.9.
  • One signal dominates (e.g. S_pc > 0.8, S_name < 0.3) → return top, confidence ~0.7, flag postcode_city_mismatch.
  • Small margin (< 0.1) with disagreement → confidence ~0.5, flag ambiguous_locality, optionally return both as an ordered list.

The three auxiliary tables — what's worth building

  • Transposed/alias citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. namesskip the dedicated table. WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. ships alt_name/name:*; edit-distance + double-metaphone over a 5-item candidate set catches the rest on the fly (see #189 for splitting alt-names into their own FTS table).
  • Misappropriated postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. → targetdon't build a table of user mistakes (intent is unsourceable). But fold the sourceable half — WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations./postal-cities postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.→multiple-postal-citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. aliases — into the candidate table. That covers "the postal citypostal cityThe city name the postal service uses for a ZIP, which can differ from the legal municipality a resident lives in — e.g. many suburbs share a larger city's postal name. A source of locality ambiguity. differs from the admin citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy."; the genuine mistakes are caught by the conflict flag.
  • Abutting/adjacent postcodespostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.a radius, not a table. Candidate generation within ~10 km already captures the neighbour; an optional build-time is_adjacent boolean only if the data demands it. The containment metric handles it for free: if the gold point is in the next town, the next town is the right answer.

Falsehoods: the part Pelias and Airmail skip

data/eval/falsehoods/postcodes.jsonl currently tests alphanumeric postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. parsingaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates. — it says nothing about postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one./citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. conflicts. We extend it with the cases that actually bite travellers and border-town residents: digit transposition (7500857008), wrong-citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy.-for-postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. (10115 München), and adjacent-town borrowing. The metric is whether the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. catches the conflict (raises the flag) rather than confidently mislocating. Clean-OA containment measures accuracy; conflict-falsehoods measures the robustness that justifies a parseaddress parsingThe process of decomposing a free-text postal address string into structured components — house number, street name, locality, region, postcode, and country — so a geocoder can resolve them to coordinates.-then-resolve pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. over a BM25 box.

Staged plan (cheapest and most decisive first)

  1. PIP-containment metric + re-measure. Implement the geometric metric (gold OA point inside resolved WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. polygon, using the WOF GeoJSONWOF GeoJSONThe raw one-feature-per-file GeoJSON distributed by Who's On First repositories — the input that Mailwoman's WOF SQLite build consumes. polygons already on disk) and re-score the current resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. on DE — no build, no retrain. Gate: if DE clears ~85%, the German wall was largely a measurement artifact and the candidate table is a refinement, not a rescue. If lower, the table is mandatory. (#273)
  2. PostcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one.localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. candidate table (offline precompute, WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. alt-name aliases folded in). Re-measure. (#274)
  3. Soft-scoring reconciliation + conflict flag in the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.. (#275; builds on #227 population tiebreak, #189 alt-names.)
  4. Conflict-falsehoods evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. — extend the falsehoods doc; gate on catching mismatches. (#276)
  5. No-postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. fallbackcountrycountryThe top-level address component (an ISO country). Closed-vocabulary, so it is best handled by a deterministic matcher feeding a proposal rather than a retrained model head.-filtered fuzzy over the alias index (where the trim's clean string earns its keep).

The parser stays a clean tokentokenOne word or subword in the tokenized input. For the neural classifier, tokens come from SentencePiece (subword units); for the rule classifiers, tokens are whitespace- and punctuation-separated words. tagger throughout. PR #272 (the trim) is retained: it doesn't move the name-match metric, but it makes the name a reliable cross-check for reconciliation and feeds the string-fallback path.

Done-with-German

At least 90% of German test addresses have their gold OA point inside the polygon of the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon.-assigned WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy., with US held above 95%, no Elasticsearch, no further parser retraining.

Durable principle

The parser extracts raw text; the resolverresolverThe component that converts parsed address components (locality, region, postcode) into coordinates by looking them up in the gazetteer. The resolver ranks candidates by name match, population, and proximity, and returns the best-matching place with its centroid or polygon. assigns authoritative geography by projecting postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. centroids onto WOFWOF (Who's On First). An open-source gazetteer of places maintained by Mapzen/whosonfirst. Mailwoman builds a custom SQLite database from WOF GeoJSON repos, extended with postcode data, importance scores, and coincident-role relations. polygons, using strings only as a tiebreaker — and when the postcodepostcodeThe country-specific postal code (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one. and the name disagree, it says so instead of guessing.

  • PR3 Pilot A eval — the negative result that pointed here.
  • Anchor-based parsing direction — the parser-side direction this complements.
  • Issues: #227 (population tiebreak), #189 (alt-names FTS table), #242 (the self-conditioningself-conditioningAn auxiliary model head that predicts the input's locale (language/country) and then FiLM-modulates the encoder so parsing adapts to the detected locale. Structural facts like word order still need explicit training on both orders. pilot — now closed negative), epic #239.