Skip to main content

Going multi-locale starts with finding out what's actually broken (2026-06-02)

You have a parser trained on US and French addresses, and you want it to read German ones. The tempting move is to grab a pile of German data and retrain. Don't. Not yet. First find out what's actually broken, because "it doesn't work on German" hides at least three different failures, and two of them aren't the parser's fault at all.

This is the write-up of that finding-out, done as a night-shift before committing a single GPU-hour.

The resolver was never the problem

The first thing that looked like a catastrophe turned out to be a measurement bug. Run a German address through the end-to-end 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. and it lands in North America: Berlin resolves to a 20,000-person Berlin in Connecticut instead of the 3.6-million-person one in Germany, a great-circle error of about 5,940 km.

That looks like 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. being hopelessly US-centric, and for about ten minutes I believed it. Then I found the evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error. harness hardcoded defaultCountry: "US", which the lookup applies as a hard filter, so every German address was restricted to US places before ranking ever ran. German Berlin was never even a candidate. Fix the one string and the same 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., against the same global 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., puts German Berlin first on population alone: coordinate error collapses from 5,940 km to 10 km. The ranking was fine the whole time. (The full autopsy is 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.-vs-evalevalRunning the model against a held-out golden dataset and computing per-component F1, exact-match, calibration, and resolved-coordinate error.-artifact notes; the short version is: when a finding feels like a crisis, check whether you're holding the instrument upside down.)

So 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. and 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. already handle German. The gap is narrower than it looked, and it lives in the parser.

What the parser actually gets wrong, and why

Here is the neural parser on real German addresses, next to the rule-based v0:

inputneuralthe bug
Straußstraße 27streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.=Strautruncated at the ß
Hauptstraße 5streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.=Hauptstraße 5house numberhouse numberThe numeric or alphanumeric identifier of a building on a street. Mailwoman's house_number component; its position relative to the street name flips between locales. swallowed into the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.
Prenzlauer Allee 36, 10405 BerlinpostcodepostcodeThe 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.=36the house numberhouse numberThe numeric or alphanumeric identifier of a building on a street. Mailwoman's house_number component; its position relative to the street name flips between locales. tagged as 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.

None of these are random. They are all one thing: order. The modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' learned addresses where the house numberhouse numberThe numeric or alphanumeric identifier of a building on a street. Mailwoman's house_number component; its position relative to the street name flips between locales. comes first (350 5th Ave) and 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. comes after the citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. (New York, NY 10118). German runs the other way: house numberhouse numberThe numeric or alphanumeric identifier of a building on a street. Mailwoman's house_number component; its position relative to the street name flips between locales. after the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. (Hauptstraße 5), 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. before the citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. (10405 Berlin). Shown a German address, the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' keeps applying the American grammar, and the tags land in the wrong slots. The 36 looks like 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. because in the positions the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' trusts, that's where a number like it would sit.

That's a coveragecoverageThe fraction of a population or region for which a data source has real, non-placeholder entries — e.g. 47% rooftop coverage on Texas addresses. Distinct from accuracy on the rows that are present. gap. But before spending money closing it, there's a cheaper thing it could have been, and you have to rule it out.

The gate: is the tokenizer the wall?

Straußstraße → Strau is suspicious. The 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. dies exactly at the ß. If the tokenizertokenizerThe component that converts a raw address string into a sequence of numeric token IDs the model can process. Mailwoman's tokenizer is a SentencePiece unigram model trained specifically on postal addresses. can't represent German orthography, if ß and the long compound streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. names fall apart into piecesECE (Expected Calibration Error). A metric that measures how well a model's confidence scores align with its actual accuracy. Lower is better. Mailwoman's held-out ECE drops from 0.067 (raw) to 0.0035 (calibrated). the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' can't reassemble, then no amount of trainingtrainingThe process of adjusting a model's parameters so its predictions match labeled examples, by repeatedly measuring error and nudging the weights to reduce it. Distinct from inference, when the trained model is run on new input. data will help, and the right move is a tokenizertokenizerThe component that converts a raw address string into a sequence of numeric token IDs the model can process. Mailwoman's tokenizer is a SentencePiece unigram model trained specifically on postal addresses. change, which is a much bigger, more expensive decision.

So that's the gate, and it's an afternoon's work, not a GPU run. Feed the v0.6.0-a0 SentencePieceSentencePieceA language-independent subword tokenizer that splits text into pieces using a unigram language model. Mailwoman uses a SentencePiece tokenizer with a 48,000-token vocabulary and byte-fallback, trained on address data rather than general text. tokenizertokenizerThe component that converts a raw address string into a sequence of numeric token IDs the model can process. Mailwoman's tokenizer is a SentencePiece unigram model trained specifically on postal addresses. a batchbatch sizeHow many examples the model processes before each parameter update. Larger batches give smoother gradients but cost more memory; gradient accumulation simulates a big batch on a small GPU. of German streetsstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. and check the round-trip:

Straußstraße → "▁Strau" "ß" "straße" → "Straußstraße" (lossless)
Karl-Liebknecht-Straße, München, Köln, Düsseldorf, Schöneberg → all lossless (10/10)

The tokenizertokenizerThe component that converts a raw address string into a sequence of numeric token IDs the model can process. Mailwoman's tokenizer is a SentencePiece unigram model trained specifically on postal addresses. is fine. ß gets its own clean piece, sitting right next to its streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. stem. So the Strau truncationtruncationCutting an input down to the max sequence length (or an LLM response to its token limit), discarding everything past the cap. is the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.''s own doing: out of distribution, it takes that piece boundary as a convenient exit and ends the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. 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. early. That's exactly the kind of mistake more trainingtrainingThe process of adjusting a model's parameters so its predictions match labeled examples, by repeatedly measuring error and nudging the weights to reduce it. Distinct from inference, when the trained model is run on new input. data fixes.

Gate passed. The wall is coveragecoverageThe fraction of a population or region for which a data source has real, non-placeholder entries — e.g. 47% rooftop coverage on Texas addresses. Distinct from accuracy on the rows that are present., and coveragecoverageThe fraction of a population or region for which a data source has real, non-placeholder entries — e.g. 47% rooftop coverage on Texas addresses. Distinct from accuracy on the rows that are present. is cheap.

Teaching the order

The fix is a small supplement shardshardA partial output file of the corpus build, written in Parquet format. The training pipeline streams shards row by row. that shows the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' German order, and the trick is to not synthesize German streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. names, because German morphology is hard to fake and you'll teach the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' your own mistakes. Instead, take real OpenAddressesOpenAddresses (OA). A global open aggregation of address points collected from many official sources. A primary source of component-supervised training data outside proprietary registries. tuples (Berlin and Saxony, 1.2 million of them) and re-render each one in idiomatic German order. The rendering is free: the OpenCage DE template already knows the convention.

{street: "Straußstraße", house_number: "27", postcode: "12623", locality: "Berlin"}
→ "Straußstraße 27, 12623 Berlin"

The aligner turns that into exactly the BIO signal the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' is missing: house numberhouse numberThe numeric or alphanumeric identifier of a building on a street. Mailwoman's house_number component; its position relative to the street name flips between locales. after the streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels., 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. before the citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy.:

Straußstraße/B-street 27/B-house_number 12623/B-postcode Berlin/B-locality

Five thousand of those, weighted at 0.2 (a supplement, not a flood), continue-trained on top of v0.7.2. One variable changed against the previous recipe.

The baseline, and an honest stopping point

Here's the "before," measured on a held-out German set the modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' has never seen:

tagv0.7.2 (out of distribution)
streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.19.1%
house_number14.6%
localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy.72.5%
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.89.0%

streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. and house_number are the floor, the two tags that depend on order. localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. and 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. are already decent, because a citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. name and a five-digit number are recognizable wherever they sit. The shardshardA partial output file of the corpus build, written in Parquet format. The training pipeline streams shards row by row. targets the floor.

The retrain itself is staged but not run as of this writing. Launching it mutates the shared trainingtrainingThe process of adjusting a model's parameters so its predictions match labeled examples, by repeatedly measuring error and nudging the weights to reduce it. Distinct from inference, when the trained model is run on new input. corpuscorpusThe BIO-labeled training data used to train Mailwoman's neural classifier. Assembled from real sources (OpenAddresses, National Address Database) and synthetic shards (boundary stress, order variants, negative space). Managed by @mailwoman/corpus. manifest, the kind of irreversible, shared-stateregionThe first-level administrative subdivision of a country — a US state, a French region, a province. The component between country and locality. change that deserves a human's yes rather than an autonomous one. Everything is one approval away: the shardshardA partial output file of the corpus build, written in Parquet format. The training pipeline streams shards row by row. is built and uploaded, the config is committed, the before/after harness is wired. When it runs, the test is simple and pre-registered: streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels. and house_number should climb, US and French should not move more than a point (the interference tripwire), and the German 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. coordinate error should hold.

If German costs US accuracy, the shardshardA partial output file of the corpus build, written in Parquet format. The training pipeline streams shards row by row. comes back out. That's the whole discipline: you don't get to hope a localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. is free, you measure whether it was.