- zipqa-intent-150k β natural-language questions over a US ZIP / county / city table, with a 148k-parameter model
- 1. Why not just train a model that memorises the table?
- 2. Architecture
- 3. Training data: generated from the table itself
- 4. Training
- 5. Evaluation on questions a person typed
- 6. What the full table forced us to fix (none of it in the model)
- 7. Every question type, verified against one row
- 8. Files
- 9. Retraining on your own table
- 10. Lineage
- 1. Why not just train a model that memorises the table?
zipqa-intent-150k β natural-language questions over a US ZIP / county / city table, with a 148k-parameter model
A from-scratch, 148,195-parameter character-level parser that turns a plain-English or Hinglish question about a US ZIP-code table into a structured query, plus a deterministic engine that answers it from the table. The model decides what is being asked; the engine guarantees every fact comes from a row. It never hallucinates a county.
"60614 ka county aur area code batao"
β parser : intent=lookup wants=[COUNTY_NAME, AERA_CODE] slots=[(ZIP, "60614")]
β engine : ZIP 60614 is in Cook County and has area code 773.
Trained on CPU (2 cores) in ~2.5 hours. Inference is ~2 ms for the model and ~100 ms for a fuzzy name lookup over 36k place names. Weights: 594 kB.
The table is not included. The model was trained against a licensed USPS-derived dataset (99,214 rows:
ZIP, STATE, STATE_NAME, CITY, CITY_ABB, LATITUDE, LONGITUDE, COUNTY_CODE, COUNTY_NAME, AERA_CODE, ZIP_TYPE, LAST_LINE_INDICATOR, FACILITY_CODE, FINANCE_NUMBER, CBSA, CBSA_TITLE, CBSA_STATUS). Any CSV with those columns works; the free HUD USPS ZIP crosswalk or the Census ZCTA relationship files can be mapped to it. The engine builds its indexes from whatever CSV you give it.
1. Why not just train a model that memorises the table?
That was the first idea β published as sraivante/zip-county-city-seq2seq-21m β and it is the wrong tool. A ~40k-ZIP table is ~100k rows of digits and proper nouns with no
pattern to generalise from. A small transformer trained to emit "72013 ARECIBO" after "zip 612" is gradient descent used
as a lossy hash map: it confuses 612/613/614 (adjacent numbers, same county β great for generalisation, fatal for exact
recall), invents plausible FIPS codes and coordinates, and has to be retrained on every table refresh.
What people actually need is: understand the question, look up the facts. Those are different problems with different right-sized solutions. Understanding ~7 question shapes over ~12 columns is a 150k-parameter job. Looking up facts is a dictionary.
2. Architecture
question (β€96 chars, lower-cased)
β
βΌ char embedding (52 symbols β 48-d)
β
βΌ 1-layer bidirectional GRU, hidden 128 ββββββββββββββ
β β
βββΊ per-character head β BIO slot tags β (ZIP, CITY, COUNTY, STATE, CBSA, AREACODE, LAT, LON)
β β
βββΊ sentence vector (final states + mean pool) ββββββββββ€
βββΊ softmax β intent (lookup, list, count, check, nearest, top, unknown)
βββΊ sigmoid β wants (COUNTY_NAME, COUNTY_CODE, STATE, CITY, ZIP, AERA_CODE,
LATLON, CBSA_TITLE, CBSA, ZIP_TYPE, ALL β multi-label)
Three heads, one loss (CE + BCE + CE). Char-level because "Mayagez", "mayagΓΌez" and "MAYAGUEZ" must land on the same
embedding and unseen place names must not become <unk>. A 2-layer GRU was 2Γ slower on CPU for no accuracy gain.
3. Training data: generated from the table itself
No human-labelled questions exist for this, so the corpus is synthesised from the table β the city, county and state names are the entity vocabulary, so coverage is exhaustive by construction. 1,000,000 examples:
| intent | share | how it is generated |
|---|---|---|
| lookup | 42% | 55 question frames Γ 11 want-columns Γ random entity; 20% compound ("county and area code") |
| list | 14% | "zips in X", "neighbourhoods of X", "which states have a Springfield" |
| check | 13% | "is X in Y", "are X and Y in the same county" |
| count | 11% | "how many zips in X", "kitne county hai" |
| unknown | 8% | weather, population, greetings, SQL, gibberish, questions about places in the table that the table can't answer |
| nearest | 6% | "closest city to 18.44, -66.40" with random coordinates in the table's bounding box |
| top | 6% | "which county has the most zips", "top 5 counties in Texas by zip codes", "sabse zyadaβ¦" |
Every example is built from labelled segments, so character-level slot tags survive the noise that is then applied: keyboard-neighbour typos, dropped/swapped/doubled letters (35% of entity tokens, 12% of frame tokens), random casing, dropped neighbourhood prefixes ("Urb Monaco 3" β "Monaco 3"), un-padded ZIPs ("603" for "00603"), chatty prefixes and suffixes ("bhai", "pls", "batao"), and Hinglish frames ("{E} kis county me hai", "{E} ka {T} batao").
Three generator decisions turned out to matter more than anything in the model:
- State qualifiers must be true. 35% of city/county mentions carry a state ("austin tx", "portland, maine"), tagged STATE β but only a state that place actually exists in. A first run without this read "springfield MA" as a metro-area question because it had never seen a state code after a city.
- Word-like state codes are never bare surfaces.
IN OR ME OK HI DE LA OH PA MA CO ID AL MO NE MD MS AR MIare English (or Spanish) words. A model that learns "miami in" β Indiana is worse than useless; the generator only emits these as full state names, and the parser accepts the 2-letter form only when written in capitals (or, for the rarer ones likeco,ma, immediately after a place name). - The unknown class must not contain in-table entities with answerable phrasings. "Tell me about {E}" was
originally an unknown example; it is a legitimate lookup (
ALL). Conversely, foreign places ("pin code of Varanasi") were removed from the unknown templates β the model should not learn that "Varanasi" means unknown, it should learn that unresolvable means unknown, which the engine decides.
4. Training
python3 prepare_data.py raw_dump.csv table.csv # clean (see Β§6)
python3 gen_corpus.py table.csv corpus.jsonl 1000000
python3 train.py corpus.jsonl parser.pt 2 # AdamW 2e-3, OneCycle, bs 256, ~75 min/epoch on 2 CPU cores
Validation on a 2% held-out split of the generated corpus:
| epoch | intent acc | wants exact-match | slot tag acc (per char) |
|---|---|---|---|
| 1 | 99.93% | 97.56% | 99.02% |
| 2 | 99.98% | 98.78% | 99.22% |
Those numbers are against generated questions and are mostly a sanity check. The number that matters is below.
5. Evaluation on questions a person typed
eval_full.py holds 48 questions written without looking at the templates β English, Hinglish, typos, compound
asks, ambiguous names, out-of-scope requests β each with the substring the answer must contain. Final score: 48/48.
which county is 90210 in β ZIP 90210 is in Los Angeles County.
Bevrly Hills california zip β Beverly Hills has ZIP codes 90209, 90210, 90211, 90212, 90213.
which county is springfield in β There are 26 places called Springfield (AR, CO, GA, β¦). Which state do you mean?
county of springfield MA β Springfield is in Hampden County.
what county is las vegas in β (assuming Las Vegas, NV; it also exists in NM) Las Vegas is in Clark County.
county of St. Louis MO β Saint Louis is in Saint Louis City County, Saint Louis County.
Cook County IL me kitne zip hai β Cook County has 228 ZIP codes.
how many states have a washington county β There is a Washington County in 31 states: Alabama (AL), β¦
top 3 states by number of zip codes β 1. Texas (2598); 2. California (2589); 3. Pennsylvania (2172).
is 10001 in the new york metro β Yes, ZIP 10001 is in the New York-Newark-Jersey City metro area.
county of 09002 β ZIP 09002 is a military (APO/FPO/DPO) address in Armed Forces Europe (AE)β¦
population of los angeles county β I can only answer questions about this ZIP table: β¦
6. What the full table forced us to fix (none of it in the model)
Going from a 1,000-row sample to 99,214 rows broke nothing in the parser and almost everything in the engine. This is the central lesson of the project: the model was never the bottleneck.
| problem at scale | fix |
|---|---|
The dump had stray " characters inside CITY_ABB and CBSA_TITLE; a standard CSV parser silently dropped 1,158 rows |
no field contains a comma, so: strip all quotes, split on commas |
CBSA_TITLE was double-encoded mojibake (San GermΓΖΓ'β¦) |
repair latin1βutf8 twice, fold to ASCII, override 4 titles by CBSA code |
ZIP stored as int: 00601 β 601 |
zero-pad on load; accept both forms on input |
| 31 Washington Counties, 26 Springfields, 6,543 city names in more than one state | state scoping: a STATE slot narrows; else if one state holds β₯80% of the ZIPs (New York, Los Angeles) assume it and say so; else ask "which state?" |
| Names that are a city and a county and a state (Miami, Vermont, Washington, Nevada) | decided from cue words ("county", "city") and position, not the tagger alone |
| A city called Many (LA) and a city called Tell (TX) | a single-word gazetteer hit is rejected if the word occurs in any question template |
| "st louis" exists literally in MS and TX; Missouri's is "SAINT LOUIS" | st/ste/mt/ft/pt aliases, applied when the state filter comes back empty |
difflib fuzzy matching: ~2 s per query over 36k names |
rapidfuzz WRatio: ~100 ms; plus a ratioβ₯80 floor so "varanasi" stops matching "Varna" |
| Military APO/FPO rows: no county, coordinates 0,0 | answered as military addresses; skipped by nearest |
| USPS label "cities" (BUSINESS REPLY MAIL, BAR CODE CHURCH STREETβ¦) | kept in the table, excluded from question generation |
| "chicago metro" vs title "Chicago-Naperville-Elgin" | CBSA prefix match wins over the exact city hit |
low.find("or") matched the "or" inside "portland" |
whole-word span expansion |
7. Every question type, verified against one row
Row: 60614, IL, Illinois, CHICAGO, 41.9238, -87.6492, 17031, COOK, 773, standard, CBSA 16980 Chicago-Naperville-Elgin.
Each line below was actually run against the published weights.
Field lookups β answered
- which county is 60614 in β Cook County Β· what state is 60614 in β Illinois (IL) Β· what city is zip 60614 β Chicago
- area code of 60614 β 773 Β· coordinates / lat long for 60614 β 41.9238, -87.6492
- what metro area is 60614 in β Chicago-Naperville-Elgin Β· cbsa code for 60614 β 16980 Β· county fips code of 60614 β 17031
- what type of zip is 60614 / is 60614 a po box zip β standard ZIP
- tell me about 60614 β full record Β· where is 60614 / 60614 kahan hai β Cook County, Illinois
- 60614 ka county aur area code batao β Cook County and area code 773 (compound)
- 60614 -> city β Chicago (terse forms work)
Same row from the city / county side β answered
- zip codes of chicago illinois β 90 ZIPs Β· how many zips in chicago β 90 Β· chicago il area code β 773, 312, 847, 708
- which county is chicago in β Cook Β· what metro is chicago in β Chicago-Naperville-Elgin Β· cook county illinois fips β 17031
- neighbourhoods in 60614 β Chicago, Lincoln Park Β· how many places are in 60614 β 2
- which state is cook county in β (assuming Cook, IL; also GA, MN) Illinois Β· zips in cook county il β 228, first 40 listed
Checks β answered
- is 60614 in cook county / in illinois / in the chicago metro / in chicago β Yes
- is 60614 in dupage county β No β it is in Cook Β· 60614 and 90210 same state? β No. IL vs CA
- are 60614 and 60657 in the same county β Yes, Cook Β· do 60614 and 60601 share a metro area β Yes
Geography β answered
- nearest zip to 41.92, -87.65 β 60614 (0.4 km) Β· what city is at 41.9238, -87.6492 β Chicago Β· which county is at 41.92,-87.65 β Cook
Aggregations β answered
- which county in illinois has the most zips β Cook (228) Β· top 3 cities in cook county by zip codes β Chicago (90), Schaumburg (8), Palatine (7)
- which city in illinois has the fewest zips β (one of many with 1) Β· how many states have a chicago β 1 Β· which states have a cook county β IL, GA, MN
Follow-ups β answered (one turn of memory)
- "which county is 60614 in" β "and its area code?" β 773 Β· "aur uska metro?" β Chicago-Naperville-Elgin Β· "what about 60657" β full record
Refused β not in the table (scope message)
- population Β· weather Β· time zone Β· median income Β· is it safe Β· crime Β· schools Β· hospitals Β· distance between 60614 and 60601 Β· driving time Β· who delivers mail Β· 60614 ka pin code india
Refused β columns deliberately not exposed (explicit message)
- finance number Β· facility code Β· city abbreviation Β· CBSA status Β· last-line indicator (internal USPS bookkeeping)
Cannot do β honest limits
- Ranges and arithmetic: "zips between 60601 and 60610", "average zips per county in illinois" β answers the nearest supported thing instead of refusing
- Filters combining two attributes: "which zips in chicago are po box only", "zips with area code 773 in cook county" β one filter is applied, the other dropped
- Comparatives: "60614 vs 60615 which is bigger" β nonsense answer
- Three-way compound wants: "county, state and metro for 60614" β usually returns one or two of the three (the corpus only has two-way compounds)
- Lowercase
in / or / me / ok / hi / de / oh / la / paare never read as states β "portland or" needsORor "oregon" - Memory is one turn deep and entity-only; no "the second one" / "the other county"
- Answers are templated English; fluent prose would need a small seq2seq writer fed the engine's facts
8. Files
| file | role |
|---|---|
model.safetensors, config.json |
the parser (148,195 params) |
model.py / schema.py |
architecture, labels |
ask.py |
QA class: parse β gazetteer backstop β scope guards β engine; CLI and interactive loop |
engine.py |
entity resolution (exact β abbreviation β fuzzy β cross-type), state scoping, the 6 intents, answer templates |
gen_corpus.py, train.py, prepare_data.py |
reproduce everything from a raw dump |
eval_full.py, eval_questions.py |
hand-typed test sets |
example.py |
10-line usage |
pip install torch rapidfuzz safetensors
python3 example.py # expects table.csv next to it
python3 ask.py # interactive
9. Retraining on your own table
The parser learns question shapes, not facts, so it transfers to any table with these columns unchanged β the
engine rebuilds its indexes from the CSV. Retrain only if your place-name distribution is very different (non-US
scripts, say): gen_corpus.py reads its entity vocabulary from the table, so the four commands in Β§4 are the whole
recipe. Adding a new question type = a few dozen frames in gen_corpus.py + a method in engine.py + retrain (~2.5 h CPU).
10. Lineage
Predecessor: sraivante/zip-county-city-seq2seq-21m, a
31M-parameter seq2seq that tried to memorise the table directly β 12.1% complete answers on fresh prompts at v0.3.0.
This repo replaces it: 200Γ fewer parameters, 48/48 on hand-typed questions, zero hallucinated facts.
Same recipe as sraivante/rx-intent-1m (drug-name lookup): a tiny
intent parser in front of a deterministic, typo-tolerant lookup engine. The thesis both models test is that for
"answer questions about this dataset", a sub-megabyte model plus a dictionary beats a large model plus RAG on
correctness, latency and cost β as long as you are honest about the question shapes you support and refuse the rest.
- Downloads last month
- 1