Install Mailwoman and parse your first address
By the end of this page you will 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 as a tree of labeled components, each with a confidence score. You do not need an API key, a server or an account. The tutorial takes about ten minutes, most of it spent waiting for the download.
Prerequisites
Check these before you start. Two of them will stop you partway through if they are not met.
- 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 put 746 MB in
node_modules. Of that, 500 MB 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. You pay this cost once per project, and 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. adds no further cost. - 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
Each package has one job. mailwoman is the pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → 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.'. @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.
None of these packages runs an install script, so the only network traffic during the install is 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 main entry point. It connects the whole staged pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-hint → 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 supply one) and returns a single async function. The classifier is the only 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). without a
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. Every other 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). has a production default.
The address is deliberately messy. It is lowercase, has no commas, and puts 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)
On the machine used to verify this page, 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. took
402 ms. 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-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package.
once and reuse it instead of rebuilding it for each request.
4. Read the tree
Know three things about that output before you write code against it.
The result is a tree. result.tree.roots is an array of top-level nodes, and
each node has tag, value, confidence, and children. The nesting represents 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 postcode (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 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., 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 nodes by tag. The parser returns the containment structure and leaves flattening to 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). To apply it, wire createCalibrator from
@mailwoman/core/decoder. The calibrator is off by default so that 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 stays
byte-stable. 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 is 0.913 against
0.980 accuracy, and the fit reduces expected calibration error from 0.0698 to 0.0017. If you plan to
route on the score (for example, sending everything under 0.8 to human review), wire the calibrator
first. The uncalibrated score would send more addresses to review than the error rate justifies.
Values come back repaired. The input said apt 4b and the output contains Apt 4B.
The ordinal 5th comes back as 5TH. If you pass the same address in mixed case with commas, the
repair step leaves the casing unchanged. If you need the exact characters the user typed, keep your own
copy of the input string, or read result.input.
Two more fields on result are useful later. 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-hint → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package.
chose, and result.kind records the query shapequery shapeStage 1.5 of the runtime pipeline: computes a structural fingerprint of the input — script class, segmentation, known-format hits (postcode regexes, state abbreviations) — in microseconds without ML. Used by downstream stages for locale detection and kind classification. it detected (structured_address here, or
postcode_only, po_box, intersection, and others).
What you have now
You have a parser running in your own process, without a service dependency. It turns free-text addresses into labeled components with confidence scores you can route on. It does not produce coordinates yet, because coordinates require 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. That database is a separate download.
Next
- Your first ten minutes — what works out of the box, what does not work 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.