You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

qwen35-address-std-4B-v5

Qwen3.5-4B fine-tuned (LoRA, merged 16-bit) for two US-address tasks, selected by the system prompt you send:

  1. Normalize -- one raw US address string -> a strict JSON object of standardized components (PostGIS address_standardizer stdaddr schema, camelCase keys).
  2. Compare -- two addresses (raw + components each) -> per-bucket match verdict (MATCH / PLAUSIBLE / NOT_MATCHED).

Trained on dataset revision v5 (pinned tags on both datasets): ~49k unique normalize rows with 100% US city coverage (all ~18.6k city names from ~30k real city/state/ZIP triples), plus 8.1k compare rows. 1 epoch, effective batch 16.

v5 output contract (important)

Empty fields are OMITTED from the output JSON (a missing key means "not present in the input"). This cuts output tokens ~43% for proportionally faster generation. Consumers must treat a missing key as empty. Example:

{"houseNum":"4422","predir":"N","name":"GROVE","suftype":"AVE","sufdir":"SW","city":"RIVERSIDE","country":"USA","zipcode":"64150"}

Results

Greedy decoding, enable_thinking=False, on the v5 validation split (2,139 rows, held out; every val city also appears in train so this measures learned accuracy, not unseen-city luck).

Normalize: exact-match 2121/2139 = 99.2% (all 16 fields correct simultaneously)

Field Acc Field Acc Field Acc Field Acc
building 100.0% pretype 100.0% ruralRoute 100.0% country 100.0%
houseNum 100.0% name 99.9% extra 99.9% zipcode 100.0%
predir 100.0% suftype 99.8% city 99.5% box 100.0%
qual 100.0% sufdir 100.0% state 99.7% unit 99.8%

No city-spelling alterations were observed in the misses (the 2B variants occasionally garbled rare city names, e.g. PATCHOGUE->PATCHOQUE; the 4B does not). Residual misses are dominated by genuinely ambiguous inputs: a bare state-name word with no ZIP ("3219 Big Spring COURT, NEVADA") can be a city (Nevada, MO) or a state -- either reading is defensible.

Version history (same harness; v4 models evaluated on the v4 split):

Model Data Normalize exact-match Notes
2B-v4 v4 (full JSON) 98.8% rare-city spelling garbles present
2B-v5 v5 (trimmed) 98.6% fastest; garbles persist (model-size floor)
4B-v4 v4 (full JSON) 99.3% garbles eliminated
4B-v5 (this) v5 (trimmed) 99.2% garble-free + 43% fewer output tokens

Comparison task (same harness, 900-row val): 99.9% exact-match across all 7 buckets on the 4B-v4 predecessor; the single miss was a boundary PLAUSIBLE/NOT_MATCHED judgment.

Usage

from transformers import AutoModelForCausalLM, AutoTokenizer
import json, re

repo = "davidr99/qwen35-address-std-4B-v5"
tok = AutoTokenizer.from_pretrained(repo)
mdl = AutoModelForCausalLM.from_pretrained(repo, device_map="auto")
# Qwen3.5 ships a multimodal processor; use the inner text tokenizer if present.
_tok = getattr(tok, "tokenizer", tok)

def run(system, user, max_new_tokens=256):
    prompt = _tok.apply_chat_template(
        [{"role": "system", "content": system}, {"role": "user", "content": user}],
        tokenize=False, add_generation_prompt=True, enable_thinking=False)
    inputs = _tok(prompt, return_tensors="pt").to(mdl.device)
    out = mdl.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
    text = _tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
    m = re.search(r"\{.*\}", text, flags=re.DOTALL)
    return json.loads(m.group(0)) if m else None

print(run(NORMALIZE_PROMPT, "155 W North St, Wooster OH 44691"))

NORMALIZE_PROMPT and COMPARE_PROMPT are the exact training prompts below (also in scripts/address_schema.py / scripts/compare_schema.py in this repo). For compare, build the user turn as:

Address A: <raw A>
A components: <normalize JSON for A>
Address B: <raw B>
B components: <normalize JSON for B>

Normalize system prompt (verbatim)

You standardize a single raw US address string into structured components.
Return ONLY a JSON object using these keys (include ONLY keys that have a value; OMIT absent fields entirely):
  building, houseNum, predir, qual, pretype, name, suftype, sufdir,
  ruralRoute, extra, city, state, country, zipcode, box, unit

Field meanings & rules:
- building: a named building/place that precedes the street address (e.g. "EMPIRE STATE BUILDING").
- houseNum: the primary street number.
- predir: directional BEFORE the street name (N, S, E, W, NE, NW, SE, SW).
- qual: a qualifier adjective before the name that is not a type or directional (e.g. "OLD", "NEW").
- pretype: street type or route descriptor BEFORE the name (e.g. "AVE", "US RTE", "STATE RTE").
- name: the core street name (or the route number for numbered routes/interstates).
- suftype: street type AFTER the name (e.g. "ST", "RD", "AVE", "BLVD").
- sufdir: directional AFTER the street (post-directional), e.g. "NW" in "Pennsylvania Ave NW".
- ruralRoute: the rural route NUMBER only (e.g. "2" for "RR 2", "1" for "HC 1").
- extra: extra information such as a floor; for an intersection it holds the cross street prefixed with "&" (e.g. "& 5TH AVE").
- city / state / zipcode: the locality, 2-letter state, and ZIP.
- country: always "USA".
- box: a post-office / route box, normalized to "BOX <number>" (e.g. "BOX 152"); drop any "PO"/"P.O." prefix.
- unit: secondary unit (e.g. "APT 5B", "STE 200"), OR any leftover token trailing the street type.

Normalization rules:
- UPPERCASE every value; abbreviate street types and directionals to USPS abbreviations.
- Preserve the original spelling of names and places: copy name, city, and other
  free-text values exactly as written (only uppercased). Do NOT fix typos, expand,
  or change spelling. (Street types and directionals are still abbreviated as above.)
- "St"/"Saint" before a name is part of the name ("ST CHARLES"), not the type "ST".
- street type before the name -> pretype; after the name -> suftype.
- directional AFTER the street (and after the suftype) -> sufdir (e.g. "NW" in "Pennsylvania Ave NW").
- numbered route -> descriptor in pretype ("US RTE", "STATE RTE", "COUNTY RD", "TWP RD", "INTERSTATE"), number (keep any letter suffix, e.g. "75A") in name.
- intersection of two streets -> primary street in predir/qual/name/suftype; cross street in
  extra as "& <CROSS STREET>" (e.g. "& 5TH AVE"); houseNum is omitted.
- any leftover token trailing the street type -> unit (whatever it is).
- the city is the locality right before the state/ZIP; output it even when it looks like a state name ("Kansas, OK" -> city KANSAS) or is short/lowercase ("ary, KY" -> city ARY).
- zipcode may include a ZIP+4 suffix (e.g. "44646-1234"); keep it if present, else use the 5-digit ZIP (omit zipcode when there is none).
- country is always "USA".
- only output fields that are present: never infer or guess a missing city, state, ZIP, or any field from the others -- OMIT the key entirely if it is not in the input.
- output ONLY the JSON object: no code fences, markdown, comments, or any text before or after it.

Compare system prompt (verbatim)

You compare two US addresses and judge whether they refer to the same place, part by part.

You are given two addresses. For each you receive the raw text and its standardized JSON components with keys:
  building, houseNum, predir, qual, pretype, name, suftype, sufdir, ruralRoute, extra, city, state, country, zipcode, box, unit

Return ONLY a JSON object with these keys, in this order:
  overall_estimate, street, house_number, city, state, zip, unit
Each value is an object of the form {"match_type": "MATCH" | "PLAUSIBLE" | "NOT_MATCHED"}.

Map the components into these buckets:
- street: name, pretype, suftype, predir, sufdir, qual -- plus building and extra (an intersection's cross street).
- house_number: houseNum -- plus box and ruralRoute.
- city: city.   state: state.   zip: zipcode.   unit: unit.
- country is always "USA" and is ignored.

match_type meaning (same logic for every bucket):
- MATCH: the two values are equivalent after standardization. Two empty values also MATCH (both agree the field is absent).
- PLAUSIBLE: cannot be ruled out as the same place. Use when one side is missing the field, or the values are a close variant: a minor spelling difference; a ZIP+4 versus the 5-digit ZIP sharing the same first five digits; a house number differing only by a letter or "1/2"; a street that matches by name but differs in type or directional.
- NOT_MATCHED: both sides have the field and they clearly conflict (different house number, different street name, different city, different state, different 5-digit ZIP, or different unit).

overall_estimate:
- NOT_MATCHED if street, house_number, city, state, or zip is NOT_MATCHED.
- otherwise MATCH if every bucket is MATCH.
- otherwise PLAUSIBLE.

Output the JSON object only.

Deployment notes

  • Missing keys = empty. Parse the output into a struct with empty-string defaults.
  • Recommended copy-guard: city and name are transcription fields -- their correct value always appears verbatim (case-insensitively) in the input. Validating that at serve time (and snapping to the nearest input span on mismatch) makes spelling drift structurally impossible.
  • Thinking mode must be disabled (enable_thinking=False / a non-thinking chat template). With thinking enabled the model emits reasoning before the JSON.
  • GGUF: convert with llama.cpp convert_hf_to_gguf.py --no-mtp (the MTP draft head is for speculative decoding only and is not needed for inference).
  • Known limitation: a bare state-name token with no ZIP ("..., NEVADA") is inherently ambiguous between city and state; expect occasional disagreement on that class.

Training

  • Base: unsloth/Qwen3.5-4B, LoRA r=16 alpha=16 on attention+MLP projections, merged to 16-bit for release.
  • 1 epoch over ~57k interleaved rows (normalize + compare), max seq 2048, effective batch 16 (2 x 8 under a 25GB VRAM cap), lr 2e-4, adamw_8bit.
  • Data generation, evaluation, and dataset-QA tooling are in scripts/ in this repo (label-first synthetic generator, PostGIS-grammar coverage oracle, regression guard, hard-case miner). Eval notebook: eval_address_model.ipynb.
Downloads last month
10
Safetensors
Model size
5B params
Tensor type
BF16
·
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for davidr99/qwen35-address-std-4B-v5

Finetuned
Qwen/Qwen3.5-4B
Adapter
(63)
this model

Datasets used to train davidr99/qwen35-address-std-4B-v5