Install Mailwoman and parse your first address
By the end of this page you'll have a Node script that takes a lowercase, comma-free, unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise.-first address and prints it back as a tree of labeled components with a confidence score on each one. No API key, no server, no account. About ten minutes, most of it waiting on the download.
Prerequisites
Read these before you start — two of them will stop you partway through if they don't hold.
- Node.js 24.18.0 or later. The
mailwomanpackage declaresengines.nodeas>=24.18.0. Check yours withnode --version. - About 750 MB of free disk. Measured on Linux x64 on 2026-08-03, the three packages below plus
their dependencies land 746 MB in
node_modules. 500 MB of that isonnxruntime-node's prebuilt binaries and 74 MB is the English 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.' package. This is a one-time cost per project, not per 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.. - An ESM project. The packages are ESM-only. Either put
"type": "module"in yourpackage.jsonor name the script file.mjs, as this page does.
1. Install
npm install mailwoman @mailwoman/neural @mailwoman/neural-weights-en-us
Three packages, three jobs: mailwoman is the 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. and the CLI, @mailwoman/neural is the
runtime that executes 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.', and @mailwoman/neural-weights-en-us is the English 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.' itself —
a 39.4 MB ONNXONNX (Open Neural Network Exchange). An open format for machine learning models that enables interoperability between training frameworks and inference runtimes. Mailwoman ships its trained model as an ONNX file so it can run in Node.js and the browser via onnxruntime. file plus its 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. and lookup tables, 73.8 MB unpacked across 16 files.
Nothing here runs an install script of ours, so nothing phones anywhere during the install beyond npm fetching tarballs.
2. Write the script
Save this as parse.mjs.
import { createRuntimePipeline } from "mailwoman"
import { NeuralAddressClassifier } from "@mailwoman/neural"
const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" })
const parse = createRuntimePipeline({ classifier })
const result = await parse("apt 4b 350 5th ave new york ny 10118")
console.log("input:", result.input)
console.log("locale:", result.locale.locale)
console.log("kind:", result.kind.kind)
console.log()
function print(node, depth = 0) {
const pad = " ".repeat(depth)
console.log(`${pad}${node.tag}: "${node.value}" (confidence ${node.confidence.toFixed(2)})`)
for (const child of node.children) print(child, depth + 1)
}
for (const root of result.tree.roots) print(root)
createRuntimePipeline is the entry point to reach for. It wires the whole staged 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. —
normalization, structural analysis, localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. detection, phrase grouping, 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.', 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.
if you give it one — and hands back a single async function. The classifier is the one stagestageOne of the dataflow stages in the runtime pipeline (normalize, locale gate, kind classify, phrase group, token classify, sequence correct, reconcile, resolve). Distinct from tier (model vocabulary) and phase (plan milestone). with no
default, 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.' is a separate download; everything else has a production default already
wired.
The address is deliberately awful: lowercase, no commas, and the unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. in front of 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..
3. Run it
node parse.mjs
input: apt 4b 350 5th ave new york ny 10118
locale: en-US
kind: structured_address
region: "NY" (confidence 0.91)
locality: "New York" (confidence 0.93)
street: "5TH" (confidence 0.93)
unit: "Apt 4B" (confidence 0.86)
house_number: "350" (confidence 0.91)
street_suffix: "Ave" (confidence 0.92)
postcode: "10118" (confidence 0.91)
Loading 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.' took 735 ms and the first 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. 402 ms on the machine this page was verified on;
the next twenty 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. on the same parse function averaged 7.3 ms each. Build the 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. once
and keep it, rather than rebuilding it per request.
4. Read the tree
Three things in that output are worth knowing before you write code against it.
It is a tree, not a flat record. result.tree.roots is an array of top-level nodes, and each
node has tag, value, confidence, and children. The nesting is geographic containment: NY
contains New York, which contains 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. 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., and 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. contains the house
number, the unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise., and the suffix. If you want a flat record, walk the tree and collect by tag —
the shape above is what containment looks like, and flattening it is your choice rather than a
default the parser makes for you.
The confidence 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 score, and it is not calibrated by default. Read 0.91 as
"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.' scored this 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. 0.91", not as "this 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. is right 91% of the time". A calibration fit
ships in the same weightsparameterA single learned number inside a model — one weight or bias. Mailwoman's encoder has roughly 30 million of them; training is the search for good values. package (calibration.json), and you apply it by wiring createCalibrator
from @mailwoman/core/decoder; leaving it out keeps 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. output byte-stable, which is why it is
off by default. The difference is measurable and it runs in one direction: on the held-out set this
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 under-confident, mean confidence 0.913 against 0.980 accuracy, and the fit moves expected
calibration error from 0.0698 to 0.0017. If you plan to route on the number — send everything under
0.8 to human review, say — wire the calibrator first, because the uncalibrated score will send you
more work than the error rate warrants.
Values come back repaired, not as typed. The input said apt 4b and the output says Apt 4B;
the ordinal 5th comes back as 5TH. Hand the same address in mixed case with commas and the
repair leaves the casing alone. If you need the exact characters the user typed, keep your own copy
of the input string — result.input has it.
Two more fields on result are worth a look when you get there: result.locale records which
localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. the 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. decided to run under, and result.kind records what shape of query it thought
it had (structured_address here, versus postcode_only, po_box, intersection, and others).
What you have now
A parser running in your own process, with no service dependency, that turns free-text addresses into labeled components with confidence scores you can route on. It does not yet produce coordinates: that 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. database, which is a separate download.
Next
- Your first ten minutes — what works out of the box, what doesn't yet, and how to check your install.
- What ships today — the per-localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. picture and the known gaps.