Skip to main content

Handle messy input

Outcome. You know which input defects the parser absorbs on its own. You know the defects that the normalizenormalizeStage 1 of the runtime pipeline: deterministic input preprocessing (Unicode NFC, punctuation normalization, whitespace collapse). Returns a NormalizedInput with an offsetMap that maps normalized positions back to the raw input. stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). removes before 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.' sees them, and when to call that stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). yourself.

For most callers the answer is short. If you use geocodeAddress, preprocessing already ran. If you call classifier.parse directly, it did not. This page explains the difference.

Prerequisites​

  • The library install from Install and first parse. Add @mailwoman/normalize if you want to call the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). directly.
  • You do not need a data root. No step on this page needs a 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..

What runs where​

The two entry points preprocess input differently:

Entry pointDeterministic preprocessingCase normalization
geocodeAddress(raw, deps)Yes — normalize() with abbreviation expansion, normalizeInput: false opts outYes
classifier.parse(raw)NoYes

Case normalization runs in both, because it is part of the classifier instead of the normalizenormalizeStage 1 of the runtime pipeline: deterministic input preprocessing (Unicode NFC, punctuation normalization, whitespace collapse). Returns a NormalizedInput with an offsetMap that maps normalized positions back to the raw input. stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone).. The other steps (Unicode form, punctuation, whitespace, abbreviation expansion) run only on the geocode path.

1. Know what the parser absorbs unaided​

Before adding a preprocessing step, check whether the defect matters. Feed 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.' the same address in three casings:

node case-compare.mjs
input 1275 Pennsylvania Ave NW, Washington, DC 20004
on region="DC" locality="Washington" street="Pennsylvania Ave NW" house_number="1275" postcode="20004"
off region="DC" locality="Washington" street="Pennsylvania Ave NW" house_number="1275" postcode="20004"
input 1275 pennsylvania ave nw, washington, dc 20004
on region="DC" locality="Washington" street="Pennsylvania Ave NW" house_number="1275" postcode="20004"
off region="dc" locality="washington" street="pennsylvania" house_number="1275" street_suffix="ave" street="nw" postcode="20004"
input 1275 PENNSYLVANIA AVE NW, WASHINGTON, DC 20004
on region="DC" locality="Washington" street="Pennsylvania Ave NW" house_number="1275" postcode="20004"
off region="DC" locality="WASHINGTON" street="PENNSYLVANIA AVE NW" house_number="1275" postcode="20004"

on is the default. off is classifier.parse(raw, { caseNormalization: "preserve" }), and it shows what 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.' does with the input casing unchanged.

Look at the all-lowercase row. With case normalization off, pennsylvania ave nw splits into two street spansspanA 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. with a street_suffix between them. With it on, the same input returns one streetstreetThe named linear feature along which house numbers are ordered. Decomposes into a name plus street affixes; one of the Tier 2 fine labels.. Real users type in lowercase, and a map search box receives lowercase input constantly, so the fix is in the shipped path instead of in your code.

One function handles both directions. normalizeInputCase (neural/case-normalize.ts) detects a pure-ASCII all-caps input and title-cases each run of three or more letters. It leaves one- and two-letter runs in capitals, so DC stays DC instead of becoming Dc and being labeled as a localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighborhood in the hierarchy.. For a pure-ASCII all-lowercase input it does the reverse: it title-cases the long runs and uppercases the short ones, so dc becomes DC. Both rewrites preserve length, and mixed-case or non-Latin input passes through byte-identical.

2. Read offsets against your string rather than against the tree's​

Case normalization has one consequence that can surprise you:

node offsets.mjs
tree.raw: "1275 Pennsylvania Ave NW, Washington, DC 20004"
region value="DC" raw.slice(38,40)="dc"
locality value="Washington" raw.slice(26,36)="washington"
street value="Pennsylvania Ave NW" raw.slice(5,24)="pennsylvania ave nw"
house_number value="1275" raw.slice(0,4)="1275"
postcode value="20004" raw.slice(41,46)="20004"

The input to that run was all lowercase. tree.raw and every value come back in the normalized casing instead of the casing you sent. Because the rewrite preserves length, start and end still index your original string. yourInput.slice(node.start, node.end) returns the user's original text whenever you need to echo it.

3. Call the normalize stage when you parse directly​

@mailwoman/normalize is StagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). 1. It consists of pure functions and uses neither a 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.' nor I/O. Four transforms run every time (Unicode NFC, a CJK fold, punctuation and whitespace), and each one adds a record only when it changed something. Two more transforms are opt-in.

Save this as normalize-demo.mjs:

import { normalize } from "@mailwoman/normalize"

const messy = " 1275 Pennsylvania Ave NW , Washington,DC 20004 "
const out = normalize(messy, { expandAbbreviations: true, locale: "und" })

console.log("raw: ", JSON.stringify(messy))
console.log("normalized: ", JSON.stringify(out.normalized))
console.log("transforms: ", JSON.stringify(out.transforms))

The transforms array records what changed, so log it when 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. surprises you. Each entry identifies the step and its effect.

This table shows what each step does, with one input per row:

InputNormalizedStep
a b a bcollapse_whitespace
O’Brien StO'Brien Stnormalize_punctuation
1275 Pennsylvania Ave NW—Washington, DC1275 Pennsylvania Ave NW-Washington, DCnormalize_punctuation
1275 Pennsylvania Ave1275 Pennsylvania Avenormalize_cjk
〒150-0001 東京都150-0001 東京都normalize_cjk

The CJK step also affects non-CJK text. It folds full-width ASCII, such as the full-width digits a Japanese form produces. It also strips the postal mark 〒, which would otherwise tokenize as an unknown byte sequence in front of the postcodepostcodeThe country-specific postcode (US ZIP, French code postal, etc.). Mailwoman handles postcode parsing entirely by rule classifier — a regex problem, not an ML one..

Two options are off by default. expandAbbreviations with locale: "und" applies the safe set for an unknown localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.. That set contains abbreviations that mean the same thing everywhere:

"12 Bd Saint-Germain, 75005 Paris" -> "12 Boulevard Saint-Germain, 75005 Paris" Bd->Boulevard
"3 Imp des Lilas, Lyon" -> "3 Impasse des Lilas, Lyon" Imp->Impasse
"1275 Pennsylvania Ave NW, Washington DC" -> unchanged

The stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). leaves Ave unexpanded because 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.' already reads it correctly. The set targets forms 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.' finds ambiguous. caseFold is the other opt-in, and you should leave it off. The classifier's own case handling is more precise, and folding first removes the case information in proper nouns and directionals.

Verify​

Run the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). on a deliberately messy string and 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. both forms to see what the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). changed:

node normalize-demo.mjs
raw: " 1275 Pennsylvania Ave NW , Washington,DC 20004 "
normalized: "1275 Pennsylvania Ave NW , Washington,DC 20004"
transforms: [{"kind":"nfc","changed":false},{"kind":"normalize_cjk","folded":1,"stripped":0},{"kind":"collapse_whitespace","runs":4}]
raw region="DC" locality="Washington" street="Pennsylvania Ave NW" house_number="1275" postcode="20004"
normalized region="DC" locality="Washington" street="Pennsylvania Ave NW" house_number="1275" postcode="20004"

Compare the last two lines. Both 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. return the same five components, so the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). did not change what 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.' found. The only difference is one value: the raw 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. keeps the double space in street, and the normalized 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. removes it. For this address, preprocessing does not change 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.. It makes the extracted value clean enough to store. If those values go into a database column or a match keycanonical keyA deterministic, normalized string representation of an address produced by @mailwoman/codex. Lowercase, abbreviation-expanded, punctuation-stripped — so '123 Main St' and '123 MAIN STREET' produce the same key. Used for blocking in the matcher., run the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). first.

Limits​

  • Zero-width characters pass through. collapseWhitespace does not count U+200B as whitespace, so a U+200B in the middle of the string passes through the stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale check, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). unchanged. 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. handled one in testing, but a match keycanonical keyA deterministic, normalized string representation of an address produced by @mailwoman/codex. Lowercase, abbreviation-expanded, punctuation-stripped — so '123 Main St' and '123 MAIN STREET' produce the same key. Used for blocking in the matcher. built from the result will contain it. Strip zero-width and bidirectional control characters yourself if your source can produce them.
  • Offsets are reliable only against the string you passed to that call. For this reason, normalize returns an offsetMap that maps normalized positions back to raw positions. If you normalizenormalizeStage 1 of the runtime pipeline: deterministic input preprocessing (Unicode NFC, punctuation normalization, whitespace collapse). Returns a NormalizedInput with an offsetMap that maps normalized positions back to the raw input. and then 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., map offsets through it instead of assuming the indexes line up.
  • Case normalization applies to pure-ASCII input only. Accented and non-Latin input keeps its case, because changing its case can change its length (ß becomes SS) and break the offset guarantee described above. An all-caps French address does not get the all-caps fix.
  • Abbreviation expansion uses a small safe set at locale: "und". A real localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. widens the set. It is not a general address-standardization pass, and it does not implement USPS Publication 28.
  • Validate an address before you use it — checking 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. once the input is clean.
  • Understand a parse — what the components and offsets mean.
  • Tune confidence thresholds — what a low score means when the input is the problem.