Rule-based classifiers
A rule classifier is a small piece of hand-written code that labelscomponent tagOne of the 33 labels in Mailwoman's address schema — street, locality, region, postcode, house_number, unit, po_box, country, venue, intersection, and others. Each parsed span carries exactly one component tag. tokenstokenOne 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.. PeliasPeliasAn open-source geocoder, Mailwoman's spiritual predecessor. and Mailwoman v1 are built almost entirely on rule classifiers. Mailwoman v2 keeps them and adds the neural classifierneural 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.' alongside — see How it works now for the hybrid.
This article walks through how rule classifiers are structured and what they are good and bad at.
One classifier per component
In Mailwoman's design, there is one rule classifier per address component type, and they all run in parallel. The interface a classifier conforms to looks roughly like:
interface RuleClassifier {
id: string // 'postcode', 'house_number', …
component: ComponentTag
classify(tokens: Token[], context: Context): ClassificationProposal[]
}
Each classifier:
- Looks at the full 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. sequence (so it has context — knowing the next or previous 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. matters).
- Returns zero or more proposals (each saying "I think tokenstokenOne 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. i..j are a
postcodewith confidence 0.95"). - Does not depend on what any other classifier returns. The merging happens in the solver.
A worked example — the postcode classifier
Probably the simplest classifier. 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. are a regex problem, not a machine learningmachine learning (ML). Building systems that learn patterns from examples instead of following hand-written rules. Mailwoman's neural classifier is trained on millions of labeled addresses rather than programmed with parsing rules. problem.
const US_POSTCODE = /^\d{5}(-\d{4})?$/ // 10118 or 10118-1234
const FR_POSTCODE = /^\d{5}$/ // 75008
const UK_POSTCODE = /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i // SW1A 1AA
classify(tokens: Token[], context: Context): ClassificationProposal[] {
const out: ClassificationProposal[] = []
for (const token of tokens) {
if (US_POSTCODE.test(token.text)) {
out.push({
span: token.span,
component: "postcode",
confidence: 0.95,
source: "rule",
source_id: "rule:postcode:us",
penalty: 0,
metadata: { country_hint: "US" },
})
}
// FR + UK regexes similarly
}
return out
}
Three things to notice:
- Confidence is not 1.0 even though the regex matches. A 5-digit number could also be a 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. (rare) or a year. The solver sees the high score but is allowed to reject it if a better story emerges from the other classifiers.
- 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. hint as metadata. The classifier sets
country_hint: "US"when the US pattern fires. The solver can use this to bias the 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. classification. - Multiple patterns coexist. If the same 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. matches both US and UK regexes, the classifier emits two proposals. The solver picks.
A harder example — the locality classifier
This one is harder because there is no regex for "is this word a citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. name?". The Mailwoman v1 localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. classifier looks up the 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. (and short n-gramsn-gramA contiguous run of n tokens (a bigram is 2, a trigram is 3). Counting n-grams is a simple, pre-neural way to model which token sequences are common. of consecutive tokenstokenOne 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.) in a Who's On First dictionary file. Pseudo-code:
const WOF_LOCALITIES: Set<string> = loadDictionary("wof/locality.txt")
classify(tokens: Token[], context: Context): ClassificationProposal[] {
const out: ClassificationProposal[] = []
for (let i = 0; i < tokens.length; i++) {
// Try 1-, 2-, and 3-token spans
for (let len = 1; len <= 3; len++) {
const candidate = tokens.slice(i, i + len)
const text = candidate.map((t) => t.text).join(" ")
if (WOF_LOCALITIES.has(text)) {
out.push({
span: { start: candidate[0].span.start, end: candidate.at(-1)!.span.end },
component: "locality",
confidence: 0.6,
source: "rule",
source_id: "rule:whos_on_first:locality",
penalty: 0,
})
}
}
}
return out
}
Notes:
- Confidence is moderate (0.6) because dictionary hits can be misleading. "New" is the start of many citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. names, so a 1-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. match on "New" is weaker than a 3-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. match on "New York City".
- N-gramn-gramA contiguous run of n tokens (a bigram is 2, a trigram is 3). Counting n-grams is a simple, pre-neural way to model which token sequences are common. sweep is how multi-word cities work. The classifier tries 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. of length 1, 2, and 3 and emits a proposal for each match.
- No exclusion logic. If "New York" matches as a localitylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy. and "New" matches as a different component, both proposals exist; the solver picks one self-consistent combination.
What rule classifiers are good at
- Determinism. Same input, same output, every time. No retraining, no random initialization.
- Easy to fix. A bug in a regex is one line. A bug in a neural 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 retraining run.
- No data required. A rule classifier ships in code. The dictionary classifiers ship a few MB of dictionaries, not a multi-GB 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.'.
- Fast. Pattern matching is microseconds per address. The whole 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. runs in milliseconds.
- Explainable. Every proposal carries a
source_id. A consumer that seesrule:postcode:uson a wrong proposal knows exactly where to look.
What rule classifiers are bad at
- The long tail. Real-world inputs do not fit predictable patterns. For every regex you write, there are inputs the regex misses.
- Context. A regex sees one 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. at a time. "Lincoln" might be a citylocalityThe city / town / settlement component of an address: a populated place sitting between region and neighbourhood in the hierarchy., a person, or a venuevenueA named, non-address place — a business, building, park, or stadium. Mailwoman's free-text point-of-interest component, added as a Tier 2 fine label.. A regex cannot tell.
- Languages. Every localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. needs its own rules, hand-written, by someone who knows that language.
- Multi-word things. N-gramn-gramA contiguous run of n tokens (a bigram is 2, a trigram is 3). Counting n-grams is a simple, pre-neural way to model which token sequences are common. sweeps work for 2- and 3-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. cases. "Saint Petersburg, FL" is fine; "Mt Tabor Forest Preserve" (a 4-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. venuevenueA named, non-address place — a business, building, park, or stadium. Mailwoman's free-text point-of-interest component, added as a Tier 2 fine label.) is harder.
- Negative evidence. Rules can say "this looks like X". They cannot easily say "this looks like X but only in the absence of Y".
These weaknesses are the motivation for the neural classifierneural 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.'. See How the model reasons.
How rule classifiers and the neural classifier coexist
In Mailwoman v2, both produce ClassificationProposals in the same shape. The policy registrypolicy registryThe per-component table that decides which classifier (rule or neural) has authority for each address component. The Ship-of-Theseus dial. decides whose proposal wins for each component:
const policy: ClassifierPolicy[] = [
{ component: "postcode", mode: "rule_only" }, // regex wins, model not even consulted
{ component: "country", mode: "rule_preferred" }, // rule wins unless rule confidence is very low
{ component: "venue", mode: "neural_only" }, // rules can't classify venues at all
{ component: "house_number", mode: "neural_preferred" }, // model is stronger here in v3.0.0
]
This is the Ship-of-Theseus pattern. The neural classifierneural 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.' earns each component one at a time. If a neural 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.' regresses, flipping back to rule_only is one line of config.
Where this lives in the code
core/classifiers/(in the v1 tree) — every rule classifiercore/policy/— the per-component policy registrypolicy registryThe per-component table that decides which classifier (rule or neural) has authority for each address component. The Ship-of-Theseus dial.core/solver/— the merger that picks self-consistent combinations
See also
- What is an address? — the components rule classifiers labelcomponent tagOne of the 33 labels in Mailwoman's address schema — street, locality, region, postcode, house_number, unit, po_box, country, venue, intersection, and others. Each parsed span carries exactly one component tag.
- How the model reasons — the learned alternative, emissions through decode
- How it works now — the hybrid in action