Knowledge Base

title-guard: Job-Title and Person-Name Validation for B2B Contact Data on Commodity Hardware

Smol LLM Lab β€” research write-up, July 2026. All numbers measured on one OVH VPS (4 vCPU, 7.6GB RAM, no GPU) β€” `bench_results.json` in the repo.

πŸ’‘ Addendum (July 15, 2026): after this write-up, the system went through an adversarial test round β€” 1,400 LLM-generated labeled cases, a fix round, and a one-shot fresh holdout. SEC golden pass is now 100% (577/577); garbage-detection F1 on the frozen adversarial set is 0.92 (titles) / 1.00 (names); hardening cost ~10% throughput (909 titles/s whole-box, 10M records β‰ˆ 3.1h). Full methodology and the generalization findings (lists decay 98%β†’12% on fresh adversarial data; statistical detectors hold at 100%): see the blog post.

Abstract

Commercial B2B contact databases charge for data quality: detecting fake names, profane or joke job titles, keyboard mash, and placeholders, and normalizing real titles to a canonical taxonomy. We show that a transparent, dependency-light ensemble β€” TF-IDF nearest-neighbor over the O*NET occupational taxonomy plus frequency tables from Census/SSA and character-bigram language models β€” reaches 99.8% acceptance on real SEC-attested executive titles, catches 12/12 synthetic garbage titles and 16/16 fake/garbage names, and processes 1,030 titles/s on a 4-core VPS. Ten million records project to 2.7 hours on hardware that costs $5/month, versus roughly $2,000 via an LLM API. We document the memory and multiprocessing pitfalls that dominated engineering time, and the class of errors (function-vs-rank confusion) that motivates an embedding layer in future work.

1. Problem

A B2B contact record has a person name and a job title, both free-text and both polluted: Chief Butthead, asdf qwerty, test, Mickey Mouse, N/A N/A. Two tasks: (1) validity β€” is this a real name / real title? (2) normalization β€” which canonical occupation does the title denote, and at what seniority? Vendors like ZoomInfo and D&B solve this at scale as a paid product; we build a miniature open version and measure exactly what it costs.

2. Data (all free, all authoritative)

Source Contents Role
O*NET 29.1 (US DOL, CC-BY-4.0) 63,934 title variants β†’ 1,016 SOC occupations Title taxonomy / NN index
SEC EDGAR Forms 3/4/5 600 real officer titles from insider filings Golden positives (legally attested, with real-world mess: EVP & CFO, "See Remarks")
US Census 2010 surnames 162,253 surnames with counts Surname frequency
SSA baby names 1880–2024 104,819 first names, 372M births First-name frequency

Gotchas: SEC EDGAR requires a User-Agent header and ≀8 req/s; SSA blocks datacenter IPs (we fetched names.zip via the Wayback Machine).

3. Method

3.1 TitleGuard

Pipeline per title: double HTML-unescape and cleaning (strip Co-) β†’ abbreviation expansion (CEO, CFO, … SEVP, PFO; 40 entries) β†’ seniority regex ladder (c-suite / vp / director / manager / senior / entry, with a (?<!vice )president guard) β†’ char(2–4) + word(1–2) TF-IDF nearest-neighbor over the 63,934 variants, cosine distance. A secondary query strips leadership rank words ("vice president of engineering" β†’ "engineering") because ONET is organized by function, not corporate rank; the better-scoring query wins. Flags: profanity set, character-bigram gibberish (mean negative log-probability > 7.0 over a model trained on the taxonomy itself), placeholder/joke regexes. Score = similarity, with a 0.45 floor for c-suite titles (underrepresented in ONET), Γ—0.7 per flag, capped at 0.05 on profanity.

3.2 NameGuard

Per name part: frequency lookup (present in Census/SSA at all β‡’ real; score 0.6 + 0.4Β·log-scaled count), cross-lookup (first names appear as surnames and vice versa), apostrophe/hyphen handling (O'Brien β†’ obrien; de la Cruz β†’ joined delacruz), suffix stripping (Jr, III, PhD). Unknown parts get partial credit from a char-bigram model trained on all 267k known names β€” a rare or foreign name is plausible language; keyboard mash is not. Flags: fake-identity list (Mickey Mouse, John Doe, Seymour Butts, …), placeholder tokens, profanity, repeated characters, N/A patterns caught on the raw string before cleaning destroys the slash. Score = geometric mean of part scores, Γ—0.7 per flag, capped at 0.05 for fake-identity/profanity.

3.3 Batch path

check_batch() performs the same per-row analysis but stacks every query (full + rank-stripped) into one sparse TF-IDF transform and one kneighbors call. Verified verdict-identical to the single path on 305 mixed titles (0 mismatches). This is the entire source of the 63Γ— speedup below β€” same math, better ordering.

4. Evaluation

Known limitations

Surface NN confuses function with rank and metaphor: Head of Growth Marketing β†’ Soil and Plant Scientists (0.60); Managing Director β†’ Producers and Directors (0.55). The validity score is still fine (these are real titles and score as such) but the canonical mapping is wrong. This is the target of C2.5: a small embedding or function-keyword layer over the NN candidates.

5. Performance (measured)

Metric Value
Index build (cold) 4.9s, ~2.3GB peak RSS
Index load (joblib cache) 1.6s, ~350MB
One-at-a-time .check() 199 ms/title
check_batch() (1 core) 315 titles/s (63Γ— the naive path)
4 independent processes (whole box) 1,030 titles/s
NameGuard (1 core) 42,771 names/s
10M titles, this box (projected from measured rate) 2.7 h
10M names, 1 core ~4 min

Engineering notes that cost real time

  1. Never `multiprocessing.Pool` with sklearn on this box. Fork inherits corrupted OpenMP thread state after any parent sklearn call (silent deadlock, 0% CPU); spawn re-imports sklearn in the child before an initializer can cap its threads (8 BLAS threads each, deadlock again). Independent OS processes launched from a shell with OMP_NUM_THREADS=1 exported before Python starts are boring and correct.
  2. sklearn's `working_memory` default is 1GB per process. kneighbors chunks its dense distance slabs by it; four workers plus their indexes OOM-killed a 7.6GB box. sklearn.set_config(working_memory=256) costs nothing measurable here.
  3. Cache the fitted index. Building TF-IDF over 63.9k docs peaks at 2.3GB; the fitted object serializes to 50MB and loads at ~350MB peak. This is the difference between "4 workers impossible" and "4 workers trivial".

6. Scale-out economics

Total work for 10M records (titles + names): β‰ˆ 9 core-hours at measured single-core rates.

Approach Wall time Cost (10M records)
This VPS (4 vCPU, $5/mo, already owned) ~2.8 h ~$0 marginal
Rented 32-vCPU dedicated cloud server (~$0.5–1.3/hr class) ~20 min \< $1
LLM API β€” Claude Haiku 4.5, Batch API βˆ’50%, cached prefix hours (batch) ~$1,800–2,300
LLM API β€” Claude Sonnet 5, Batch API βˆ’50% hours (batch) ~$6,800
Hybrid: title-guard everything, LLM only the low-confidence ~5% tail ~3 h ~$110

LLM figures assume ~150 input + ~60 output tokens/record at July 2026 list prices (Haiku 4.5 $1/$5 per MTok, Sonnet 5 $3/$15, batch βˆ’50%). The assigned budget of $100–500 was two to three orders of magnitude above the classical-ensemble cost; even the quality-maximizing hybrid fits inside it 4Γ—.

Do you need a GPU? Not for this workload. String cleanup, regex, sparse TF-IDF NN, and frequency lookups are memory-bandwidth-bound tabular work where CPU + vectorization (and, at larger scale, Polars/DuckDB) win on cost. A GPU pays off when the per-record kernel becomes dense math β€” embedding models or transformer inference, e.g. the C2.5 disambiguation layer β€” and then the stack is Python all the way (RAPIDS cuDF/cuML, PyTorch); no CUDA C++ required. Spot A10/RTX 4090 class GPUs run ~$0.30–0.60/hr, so even that layer stays in single-digit dollars for the 5% tail.

7. Conclusion

Data-quality filtering for B2B contacts does not need a GPU, a vendor, or an LLM for the bulk of the volume. Free authoritative taxonomies plus sparse-vector retrieval get 99.8% golden-pass quality at 1,030 titles/s on a $5 box; the LLM belongs at the ambiguous tail, where it is 20Γ— cheaper than running it everywhere. The dominant engineering risks were operational (OpenMP Γ— fork, sklearn working memory), not statistical.

Reproduce: `github-less` β€” the repo lives on the box; `eval.py`, `eval_names.py`, `bench.py`, `bench_mp.sh` regenerate every number above. Data files download from ONET, SEC EDGAR, Census, and SSA (Wayback for the last).*