- Python 100%
The previous checker only knew string hygiene (HTML, placeholders, legal
suffixes, length, trigrams). It had no rule for what a job title *is*, so it
scored "Sales Department", "New York", "123 Main Street", "Test Test", "Male"
and "Manager Manager Manager" as VALID 78-100, while real titles like
"Prokurist" sat in QUESTIONABLE. Its 2.3% error rate was an artifact: the only
negatives in the benchmark were SEC placeholders, Norwegian company names and
character-level corruptions of real titles.
Rebuilt around one rule: a job title is a noun phrase whose HEAD is an
occupational role noun ([SENIORITY]* [DOMAIN]* HEAD [SCOPE]*). Language-
independent in form; only vocabulary changes.
- ontology/role_heads.json 2,645 role nouns across 24 language groups,
166 agent-noun suffixes, 113 canonical
multi-word roles, 35 role acronyms (authored,
not scraped, so the corpus stays independent)
- ontology/modifiers.json 426 domains, 127 seniority, 47 connectors
- ontology/negative_lexicon.json 13 named negative classes
- rules/title_grammar.py the engine; every verdict carries a rule trace
- benchmark/hard_cases.jsonl 588 authored hard cases (person names, org
units, places, addresses, credentials,
statuses, brands, prose) — the dirt the old
evaluation never tested
- evaluate_grammar.py scores both engines side by side
Policies: INVALID requires a named rule (no silent rejection); no head found
means REVIEW, never reject (no inventory covers every language).
Measured on 9,096 cases (8,000 held-out real titles, 508 real SEC/Brreg
strings, 588 authored):
accuracy on decided cases 100.0% (was 98.1%)
hard errors 0 (was 133)
junk leaked as VALID 0 (was 130 of 744)
sent to review 11.4% (was 21.4%)
VALID threshold calibrated at 60 (cheapest cut with zero leakage).
Also fixed: \w tokenisation shredded Indic/Thai/Arabic titles; NFKC decomposed
Thai SARA AM out of the lexicon; accent stripping destroyed Indic vowels;
2-char suffixes made Equinor/Berlin/Okafor "role nouns"; keyboard-run detection
flagged "Property Underwriter"; "i am" substring made "estrattore di amido"
prose; Romance "responsable de" was treated as prose rather than a title head.
168 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGY4Qad6trDpppS7itm7FL
|
||
|---|---|---|
| benchmark | ||
| data/real | ||
| data_sources | ||
| docs | ||
| ontology | ||
| results | ||
| rules | ||
| sample_data | ||
| tests | ||
| .gitignore | ||
| build_benchmark.py | ||
| build_hard_benchmark.py | ||
| build_lexicon.py | ||
| cli.py | ||
| country_matrix.py | ||
| evaluate.py | ||
| evaluate_grammar.py | ||
| evaluate_real.py | ||
| evaluate_title_validation.py | ||
| evaluate_validity.py | ||
| expand_ontology.py | ||
| README.md | ||
| requirements.txt | ||
Global job-title validation — explicit business rules
Product question: given a string from a contact record, filing or vendor feed, is it a real job title?
Answer: VALID / REVIEW / INVALID, a score 0–100, and the named
rule behind the call. Deterministic, inspectable, no ML.
$ python cli.py --single "Head of Growth" --explain
'Head of Growth' -> VALID (83) class=TITLE
head: head [en]
T-UNICODE clean NFKC + whitespace
T-HEAD +55 head 'head' [en]
T-DOMAIN +10 business domain: growth
T-FRAME +8 canonical title frame
T-COVERAGE +10 every token explained by the grammar
$ python cli.py --single "Sales Department" --explain
'Sales Department' -> INVALID (85) class=ORG_UNIT
T-UNICODE clean NFKC + whitespace
T-ORG-UNIT HARD INVALID org-unit noun with only business modifiers
Start here
| Doc | What it is |
|---|---|
| docs/TITLE_VALIDATION_RULES.md | The product. Every rule, its weight, its evidence, and what the rules honestly cannot do |
rules/title_grammar.py |
the engine — one rule per named T-… id |
ontology/*.json |
the three evidence files the rules read |
evaluate_grammar.py |
scores the rules against real held-out titles, real dirt, and hard negatives |
The rule everything hangs off
A job title is a noun phrase whose HEAD is an occupational role noun.
[SENIORITY]* [DOMAIN]* HEAD [SCOPE]* Senior Marketing Manager EMEA Chief Financial Officer Kaufmännischer Leiter 首席 财务 官
The form is language-independent; only the vocabulary changes. Germanic and
Finnic languages fuse it into a compound (Vertriebs·leiter), CJK and Korean are
head-final (영업·팀장), Romance and Semitic are head-first (مدير عام).
Two policies keep it honest:
- A string is INVALID only when a named rule says so. Nothing is rejected for scoring low.
- No head found ≠ junk. No inventory covers every language, so an
unrecognised string goes to
REVIEW, never to the bin.
Results
python evaluate_grammar.py — 9,096 cases: 8,000 held-out real titles (never
seen by the lexicon), 508 pieces of real production dirt (SEC placeholders,
Brønnøysund company names), 588 authored hard negatives.
| grammar rules | previous engine | |
|---|---|---|
| Accuracy on decided cases | 100.0% | 98.1% |
| Hard errors (VALID ↔ INVALID) | 0 | 133 |
| Sent to review | 11.4% | 21.4% |
| Junk leaked as VALID (of 744) | 0 | 130 |
Every rejection names a class, and every class is caught:
| Class | recall | Class | recall | |
|---|---|---|---|---|
| ORG_UNIT | 100% | COMPANY | 98.9% | |
| PLACE | 100% | PERSON_NAME | 97.7% | |
| DEMOGRAPHIC | 100% | EMPLOYMENT_STATUS | 97.0% | |
| DESCRIPTION | 100% | CREDENTIAL | 96.3% | |
| TEST_DATA | 100% | GIBBERISH | 94.4% | |
| PLACEHOLDER | 99.3% | ADDRESS | 93.3% |
Review rate by language, held-out: en 2% · fr 3% · es 3% · it 6% · nl 15% · de 20% · fi 23% · hu 32% · mt 37% · da 41%. Where head coverage is thin the cost is review, never rejection — the fix is curation, not a model.
The 588 hard negatives were authored alongside the rules, so that slice is a design target. The independent evidence is the 8,000 held-out taxonomy titles and the 508 real SEC/Brreg strings — no rule was written against either.
Quick start
pip install -r requirements.txt
python cli.py --demo # mixed real/junk table
python cli.py --single "Chief Banana Officer" --explain
python cli.py --input records.json --output out.json # batch
python build_hard_benchmark.py # rebuild the hard cases
python evaluate_grammar.py # rescore both engines
python -m pytest tests/ -q # 168 tests
What it does on the strings that break naive checkers
| Input | Verdict | Score | Why |
|---|---|---|---|
Chief Executive Officer |
VALID | 93 | canonical multi-word head |
Zimmerbereichsleiter |
VALID | 100 | German head-final compound |
ผู้จัดการฝ่ายขาย |
VALID | 77 | Thai role morpheme, no word spaces |
मुख्य कार्यकारी अधिकारी |
VALID | 85 | Devanagari, combining marks preserved |
Freelance Photographer |
VALID | 81 | qualifier + role head |
Sales Department |
INVALID | 85 | ORG_UNIT |
John Smith |
INVALID | 78 | PERSON_NAME (beats the smith head) |
New York |
INVALID | 82 | PLACE |
Equinor ASA |
INVALID | 92 | COMPANY |
Bachelor of Science |
INVALID | 84 | CREDENTIAL |
Unemployed |
INVALID | 90 | EMPLOYMENT_STATUS |
Male |
INVALID | 88 | DEMOGRAPHIC |
Manager Manager Manager |
INVALID | 93 | GIBBERISH |
See Remarks |
INVALID | 98 | PLACEHOLDER |
addetto alla spa |
VALID | 81 | not S.p.A. — Italian spa attendant |
Purple Elephant Officer |
REVIEW | 57 | valid head, two stacked unknown modifiers |
Chief Banana Officer |
VALID | 78 | valid head, one free modifier — see below |
What the rules cannot do
No rule separates Foundation Director from Chief Banana Officer: both are a
valid head plus a modifier no lexicon lists, and real titles constantly invent
modifiers (pharmacovigilance, growth, happiness). One free modifier is
therefore normal; two stacked unknowns is the measurable signal of a made-up
phrase. If the first case must be caught, that is where a semantic check belongs
— and docs/TITLE_VALIDATION_RULES.md §8 says which kind, for each gap.
| Need | Recommendation |
|---|---|
| Refute a well-formed invented title | LLM / embedding check on the REVIEW slice only |
| Thin head coverage (mt, da, hu) | curate head nouns from ESCO — no model |
| Cross-lingual role mapping | multilingual embeddings — different product |
| Company detection beyond suffixes + brands | company-registry lookup or vendor NER |
| C-suite / seniority | parked at rules/engine_v3.py — different question |
Data
| Source | Rows | Role here |
|---|---|---|
| ESCO (EU) | 53,869 | multilingual official occupations, 27+ languages |
| O*NET (US DOL) | 11,560 | official US titles |
| Wikidata | 2,362 | multilingual role labels (Asia, Africa, LatAm) |
| Brønnøysund (NO) | 4,021 | real roles and company names (company ≠ title) |
| SEC EDGAR | 2,505 | production dirt: placeholders, HTML entities |
~74k observations · 54 countries · 44 languages · 6 continents. Pull scripts in
data_sources/; rebuild the taxonomy lexicon (train split only) with
python build_lexicon.py.
The role-head, modifier and negative lexicons are authored, not derived from this corpus — which is what keeps the corpus usable as an independent test set.
Repo map
rules/title_grammar.py the engine (this is the product)
rules/title_validator.py superseded string-hygiene checker, kept for comparison
ontology/role_heads.json role nouns, morphology, canonical multi-word roles
ontology/modifiers.json seniority / domain / connectors
ontology/negative_lexicon.json the 13 things a title is not
benchmark/hard_cases.jsonl authored hard negatives + global positives
evaluate_grammar.py both engines, per-class metrics
results/real/ measured output, including threshold calibration
docs/TITLE_VALIDATION_RULES.md the rulebook
rules/engine_v3.py parked C-suite classifier (different product)
Run the old engine for comparison with python cli.py --demo --legacy.
License
MIT (see LICENSE if present). Upstream data under their own licenses
(ESCO, O*NET CC BY, Wikidata CC0, Brønnøysund NLOD).