aildan's picture
countrycode-small-v1.2.1
8f14786 verified
|
Raw History Blame Contribute Delete
15.8 kB
---
license: apache-2.0
language: [multilingual]
library_name: onnx
pipeline_tag: text-classification
tags: [country-codes, iso-3166, entity-normalization, selective-prediction, calibration, abstention, kyb, onnx]
base_model: jhu-clsp/mmBERT-small
---
# CountryCode-Decide (small, v1.2.1)
Maps a country or US state written as free text — any language or script, misspellings,
abbreviations, historical names, register junk — to ISO 3166-1 alpha-2 / ISO 3166-2:US codes with
calibrated probabilities, and **abstains** when the requested error rate cannot be met.
75 MB, about 4 ms per string on a CPU.
Code, evaluation and documentation: https://github.com/aliildan/country-code-decide · [Paper](https://doi.org/10.5281/zenodo.23289343)
## Intended use
Normalising country / jurisdiction / nationality fields in company-register data, KYB onboarding
and similar back-office pipelines, with a person reviewing abstentions and candidate lists.
**Out of scope:** full postal addresses (the model reads one field), subdivisions other than US
states, legal advice, any decision about a person without human review.
## How to use
Serving needs only `onnxruntime`, `tokenizers` and `numpy` (no PyTorch). The decision logic
(calibration, thresholds, candidate lists) lives in the repository's `CountryCoder`:
```bash
git clone https://github.com/aliildan/country-code-decide && cd country-code-decide
uv sync --no-default-groups --group serve && source .venv/bin/activate
uvx --from huggingface_hub hf download aildan/country-code-decide-small --exclude model.onnx \
--local-dir models/country-code-decide-small
```
```python
from countrycode.infer.pipeline import CountryCoder
coder = CountryCoder.load("models/country-code-decide-small")
d = coder.code("Untted Klngdom", field="country", domain="register", alpha=0.05)
d.target, d.candidates, d.abstain # ('GB', ('GB',), False); d.confidence ≈ 0.95
```
Input: `text`, optional `field` (`country`, `jurisdiction`, `nationality`, `unknown`) and
`register_country` (ISO alpha-2 of the register, or `US-XX` for a US state register). Output per
string: `target` (alpha-2, `US-XX`, `NONE` or `None` = abstain), `country`, `us_state`,
`confidence`, `candidates` (codes with calibrated probability ≥ 0.05, at most three), `abstain`,
for an error-rate target `alpha` (certified targets — curated: 5 %; register: 5 %; `None` always answers). Pass
`domain="register"` for register and form fields: the certified register calibration applies only
then; without a domain the stricter of the two policies applies, which is conservative but not
itself certified.
## Files
| File | Content |
|---|---|
| `model.int8.onnx` | the model: encoder + two heads, 8-bit weights (served; the CPU runtime computes its matrix products with int8 activations) |
| `model.onnx` | the same model in fp32 (reference) |
| `tokenizer.json`, `tokenizer_config.json` | the trimmed mmBERT tokenizer |
| `calibration.json` | temperatures, certified thresholds and candidate rule per domain (`register`, `curated`) |
| `meta.json` | label space, code names, context format, training run, quality-gate status |
## Training data
The model was trained on three kinds of strings. **About a third of the training examples are
generated (synthetic)**; no generated string is used for calibration or evaluation.
- **Lexicon** — 372,683 names in 1,274 languages (CLDR 47, Wikidata, codes, reviewed overrides); 86,406 distinct keys, 1,947 kept as genuinely ambiguous.
- **Real register strings** of the training split (Colorado, Connecticut, UK Companies House,
Brønnøysund), labelled by a person or by the register's own code; no normalised key shared with
any evaluation part.
- **Generated variants**, one release per generator, each made by code from a lexicon name with a
known code (so the label is known by construction):
- `keyboard` — typos on real keyboard layouts chosen by the name's language, with the number and
kind of edits fitted to real register typos;
- `format` — case, spacing, comma inversion, postcode noise, field truncation, abbreviation
prefixes (labelled with every code whose name starts that way);
- `translit` — romanisations and umlaut conventions;
- `geo` — a country or US state written with one of its GeoNames regions or cities;
- `pairs` — two countries in one string (labelled as the set of both);
- `affix` — a country with a compass or region qualifier;
- `caprov` — Canadian province codes in a US register's jurisdiction field (→ `CA`);
- `junk`, `regjunk` — strings that are not places: placeholders, dates, numbers, generated
company names, legal terms, street lines (→ `NONE`).
Every release the model uses passed the quality gates before training: **G1** label correctness (each string
checked against every name of every code; strings closer to another code are dropped) plus a
human audit of 300 random items per release (every item when a release is smaller) with zero
label errors required; **G2** realism (edit statistics compared with real typos — reported, no
pass/fail threshold); **G3**
usefulness (kept only if it improved held-out real strings; the OCR, LLM-paraphrase and
region generators failed this and are not used); **G4** no overlap with any evaluation string; **G5** reproducibility
(code, config, seed and source item recorded per string).
| generator | training examples | human audit (errors / audited) |
|---|---|---|
| junk@2 | 1,375 | 0 / 300 |
| keyboard@2 | 9,812 | 0 / 300 |
| format@4 | 13,411 | 0 / 300 |
| translit@4 | 4,396 | 0 / 300 |
| geo@2 | 7,381 | 0 / 300 |
| pairs@3 | 3,988 | 0 / 300 |
| affix@2 | 785 | 0 / 300 |
| caprov@1 | 78 | 0 / 39 |
| regjunk@1 | 2,731 | 0 / 300 |
- 142,925 training examples: 98,517 lexicon names and codes (68.9 %), 43,957 generated (30.8 %), 451 real register strings (0.3 %).
- Real register strings are labelled by a person or by the register's own code; none shares a normalised key with any evaluation part.
- No personal data: register sources were read with aggregate queries (distinct value and count),
never records; generated company names come from word lists.
## Evaluation
Fresh registers never used for training, calibration or model selection (New York, Oregon): every
distinct string of their jurisdiction and country fields, labelled by exact lexicon match or by a
human reviewer, read once after a frozen pre-registration (release 1.2). Release 1.2.1 keeps the
weights and corrects the certificate after an external review, so its numbers below are post-hoc.
| Fresh registers, 361 distinct strings | CountryCode-Decide small v1.2.1 |
|---|---|
| Accuracy, every string answered | 88.6 % (95 % CI 85.3–91.7) |
| Right country (a US state counts as US) | 91.2 % |
| At the certified 5 % error target | answers 88 % automatically, realised error 2.5 % |
| Best non-LLM baseline (exact lexicon + context rules) | 89.5 % |
| Zero-shot LLM (qwen3.5_35b-a3b-q4_K_M) | 85.0 %, ~29× slower (LLM on a GPU, this model on a CPU) |
| Messy register strings (656): lexicon alone → lexicon, then model | answers 24.8 % → 79.0 % (error 4.8 %) |
| Model file · CPU latency | 75 MB (int8 ONNX) · p50 4.1 ms per string |
| Predictor | automatic labels (exact lexicon match, n = 261) | hand-labelled (n = 100) | hand-labelled answered |
|---|---|---|---|
| CountryCode-Decide (always answers) | 99.2 % | 61.0 % | 100 |
| CountryCode-Decide at the certified 5 % target | 98.1 % | 56.0 % | 61 |
| Zero-shot LLM (qwen3.5_35b-a3b-q4_K_M) | 94.3 % | 61.0 % | 97 |
| Lexicon + context rules | 99.6 % | 63.0 % | 62 |
| multilingual-e5-small nearest name | 100.0 % | 37.0 % | 100 |
| exact lexicon match | 100.0 % | 24.0 % | 23 |
Automatic labels come from an exact lexicon match, so a lexicon is right on them by construction; on
the hand-labelled strings the model, the LLM and an exact lexicon with the training context rules are
about level. On held-out strings of the training registers (656 strings, many misspelled or abbreviated; other strings of the same registers were in training) the model is right on 82.6 %, the best other predictor (qwen3.5_35b-a3b-q4_K_M zero-shot) on 73.8 %, the lexicon with context rules on 35.8 %; on the 472 of them with no exact lexicon match 81.1 % against 73.1 %. On GBIF's curated list the LLM is better: 69.6 % against 58.8 %.
Lexicon first, then the model at its 5 % target:
| Set | Lexicon + context rules alone (answered, error) | Lexicon, then the model (answered, error) | Model on what the lexicon cannot read (answered, wrong) |
|---|---|---|---|
| Fresh registers (New York, Oregon), 361 strings | 89.5 %, error 1.5 % | 91.7 %, error 3.0 % | 8 of 38, 5 wrong (62.5 %) |
| Held-out strings of the training registers, 656 strings | 24.8 %, error 1.8 % | 79.0 %, error 4.8 % | 355 of 493, 22 wrong (6.2 %) |
| GBIF curated list, 3,980 strings | 17.8 %, error 2.4 % | 39.6 %, error 4.8 % | 867 of 3,270, 59 wrong (6.8 %) |
The model roughly triples what is answered automatically on messy register strings, but on what the
lexicon cannot read its 5 % certificate does not hold (last column); treat those answers as
suggestions for review until the residual has its own calibration.
- Country-level accuracy (right country; a US state counts as US): 0.912 (highest: v1_model 0.921).
- On the 33 strings not in the lexicon (no exact name match) the model's accuracy is 0.212.
- At the certified 5 % error target the model answers 88% of the strings automatically with a realised error of 2.5 %; non-places get a code 0.0 % of the time (n = 4). The 5 % target is certified over 318 register units (one per register and normalised key); 1 % and 2 % are not.
- The zero-shot LLM (qwen3.5_35b-a3b-q4_K_M) is less accurate when every string must be answered (0.850 vs the model's 0.886); its own confidence covers 71% of the strings at 5 % empirical risk, without a certificate. It is ~29× slower (the LLM on a GPU, this model on a CPU).
The guarantee is an average over strings like the calibration strings, and the errors concentrate on hard ones: at the 5 % target 13.1 % of the 61 answered hand-labelled strings are wrong, 0.0 % of the 256 answered strings that an exact lexicon match labelled.
**The certificate, corrected.** The pre-registered certificate treated the 592 register calibration strings as independent draws and certified 2 % and 5 %. The same string often appears in several fields of a register, so they are not independent: they form 318 register-and-key units. Counted per unit — a correction made after the test read, prompted by an external review — 2 % is not certified, and 5 % is, at λ = 0.762. This release (small-v1.2.1) ships that calibration; the weights are unchanged.
| predictor | n | acc | acc (freq-weighted) | country acc | coverage @1 % risk | coverage @5 % risk | non-place false accept | ECE | ms / item |
|---|---|---|---|---|---|---|---|---|---|
| lexicon_context | 361 | 0.895 | 1.000 | 0.901 | 0.000 | 0.895 | 0.250 | 0.015 | 0.0 |
| model | 361 | 0.886 | 1.000 | 0.912 | 0.856 | 0.911 | 0.750 | 0.058 | 4.2 |
| v1_model | 361 | 0.884 | 1.000 | 0.921 | 0.197 | 0.864 | 1.000 | 0.253 | 3.9 |
| model@0.05 | 361 | 0.864 | 1.000 | 0.873 | 0.856 | 0.878 | 0.000 | 0.033 | 4.2 |
| llm_qwen3.5_35b-a3b-q4_K_M | 361 | 0.850 | 0.997 | 0.858 | 0.000 | 0.709 | 0.000 | 0.108 | cached |
| e5_knn | 361 | 0.825 | 0.995 | 0.847 | 0.091 | 0.825 | 0.750 | 0.108 | 0.6 |
| lexicon_fuzzy | 361 | 0.795 | 0.994 | 0.802 | 0.000 | 0.820 | 0.500 | 0.054 | 1.0 |
| lexicon_exact | 361 | 0.789 | 0.994 | 0.793 | 0.000 | 0.787 | 0.250 | 0.014 | 0.0 |
| ons_style | 361 | 0.787 | 0.994 | 0.793 | 0.000 | 0.792 | 0.500 | 0.044 | 0.3 |
| countrynames | 361 | 0.634 | 0.761 | 0.745 | 0.000 | 0.000 | 0.000 | 0.226 | 2.7 |
| pycountry | 361 | 0.634 | 0.761 | 0.776 | 0.000 | 0.000 | 0.250 | 0.184 | 1.5 |
| coco | 361 | 0.596 | 0.761 | 0.601 | 0.000 | 0.000 | 0.250 | 0.120 | 0.5 |
| model@0.01 | 361 | 0.011 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | – | 4.1 |
| model@0.02 | 361 | 0.011 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | – | 4.1 |
Pre-registered hypotheses of release 1.2, as read (P1 and P2 rest on the item-level certificate
corrected above):
| hypothesis | result | evidence |
|---|---|---|
| P1 | met | certified=True, lambda=0.827, answered_n=309, errors=3, p_value_risk_le_2pct=0.947, contradicted=False |
| P2 | met | answered=0.856 |
| P3 | met | size_mb=74.732, cpu_p50_ms=4.092, tokens_identical=1.000, select_accuracy_drop=0.003 |
| P4 | met | model_accuracy=0.886, best_non_llm=e5_knn, best_non_llm_accuracy=0.825 |
| A1 | reported (not judged) | certified=False, answered=0.000, risk_answered=nan |
Gold labels: The register calibration strings were labelled a second time, blind (no proposal and no first label shown): agreement 99.2 % on 592 strings (5 disagreements); one-sided 95 % upper bound on disagreement 1.8 %. On the 218 hand-labelled strings the passes disagree on 5 (2.3 %); the 374 automatic or preset labels agree by construction. Test labels were labelled once.
Latency and size: p50 4.1 ms, p95 5.1 ms per string on CPU (4 threads, ONNX Runtime, tokenisation included); model file 75 MB.
## Limitations and risks
- For clean fields an exact lexicon with context rules is as accurate as this model; use the model
for what a lexicon cannot read, but note that its certificate does not cover that residual (see
the cascade table). Its advantage was measured on held-out strings of registers whose
other strings were in training; on fresh registers, strings with no exact lexicon match are hard.
- The certificate holds on average for strings drawn like the calibration strings (register
fields), counted per register and normalised key, each target on its own; not on the hard strings
a lexicon cannot read (with probability ≥ 90 % over the
calibration draw); errors concentrate on hard strings. Recalibrate before relying on it for a
different source. The curated GBIF domain is much harder than registers for this model, and there
a 35B LLM is more accurate.
- One human reviewer labelled the register strings; the calibration strings were labelled twice by
that reviewer, the second time blind (consistency, not inter-annotator agreement); test labels
were labelled once.
- Of the 4,997 evaluation strings, 5 are not in Latin script, so accuracy on other scripts is not measured.
- One training seed (13); the usefulness gate compared runs whose spread is about its margin.
- Ambiguous names are returned as candidate lists; downstream systems must not silently pick one.
- Known gaps: bare Canadian province codes in US jurisdiction fields (`ON`, `BC`) are not recognised
(every real such string is an evaluation string, so the no-overlap gate removed them from the
generated province variants); `Congo` without context is answered as `CG`.
- Not legal advice; keep a person in the loop.
## Citation
```bibtex
@misc{ildan2026countrycode,
title = {From Messy Country Names to ISO 3166 Codes: A Small Model That Abstains, and When a Lexicon Is Enough},
author = {Ildan, Ali},
year = {2026},
publisher = {Zenodo},
doi = {10.5281/zenodo.23289343}
}
@software{ildan2026countrycode_software,
title = {CountryCode-Decide: messy country names to ISO 3166 codes, with calibrated abstention},
author = {Ildan, Ali},
year = {2026},
url = {https://github.com/aliildan/country-code-decide},
doi = {10.5281/zenodo.23289336}
}
```
## Attribution
Unicode CLDR (Unicode License v3) · Wikidata (CC0) · GeoNames (CC BY 4.0) · GBIF parsers
(Apache-2.0) · Colorado, Connecticut and Oregon open data (public domain) · UK Companies House
(OGL v3) · Brønnøysund Register Centre (NLOD) · New York open data (OPEN-NY terms, evaluation only)
· mmBERT (MIT).