File size: 15,841 Bytes
726b7b1 a99caf7 726b7b1 53bbcbc 726b7b1 561e213 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 8f14786 726b7b1 a99caf7 8f14786 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 8f14786 a99caf7 8f14786 a99caf7 726b7b1 a99caf7 726b7b1 a99caf7 726b7b1 53bbcbc 726b7b1 f22e340 726b7b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | ---
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).
|