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
- SEC golden titles: 99.8% score β₯ 0.4 (the pass threshold). The failures are explainable ("See Remarks"-style artifacts).
- Garbage titles: 12/12 caught (profanity, gibberish, joke, placeholder).
- Legit names: 16/16 pass β including
Sean O'Brien,Anne-Marie Johnson,Lakshmi Venkatasubramanian,Guadalupe de la Cruz,J. Robert Oppenheimer. - Garbage names: 16/16 caught β including
Firstname Lastname,N/A N/A,xXx GamerBoy. - The composition argument:
Mickey Mouse, Chief Magic Officerpasses the title check (0.82 β it is a plausible title) and fails the name check (0.05, fake-identity). Either filter alone leaks; the product of both is the verdict.
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
- 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=1exported before Python starts are boring and correct. - sklearn's `working_memory` default is 1GB per process.
kneighborschunks 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. - 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).*