Address normalization
Real-world address strings are messy — CRMs double-concatenate fields, dropdowns get mis-selected, country tokens get appended. TaxQL’s address and ZIP modes accept that mess: by default they normalize what is unambiguously fixable, serve the lookup, and tell you what changed so you can fix the source. Callers who prefer to fail loudly can opt into strict mode.
What we fix automatically
Section titled “What we fix automatically”In the default (permissive) mode, these repairs happen before the lookup and are disclosed in the response (see Reading the disclosure):
| What we detect | Example in | Served as |
|---|---|---|
| A doubled ZIP+4 suffix (even mismatched or tripled) | …, WA 98040-4629-4629 | 98040-4629 |
| A trailing country token after a ZIP | …, TX 77002 USA | …, TX 77002 |
| A full state name or alias | …, Houston, Texas 77002 | TX |
| A duplicated street fragment | 12301 Twin Creek CIR, 12301 twin creek cir, … | one copy |
| Redundant whitespace | 1234 Main St, … | collapsed |
| A state field that’s unusable or wrong for the ZIP | …, Lincoln, AL 01773 (a MA ZIP) | MA (see below) |
ZIP-authoritative state resolution
Section titled “ZIP-authoritative state resolution”The last row is the important one. When the state field and the ZIP disagree —
or when the state field is an unusable placeholder like N, false, -, or
M — TaxQL treats the ZIP as authoritative and serves the ZIP’s state.
Why the ZIP wins: a ZIP belongs to exactly one state (it’s deterministic), state codes are hand-entered or dropdown-selected (so they carry a much higher error rate), and the ZIP is already the primary key for jurisdiction resolution. Destination-based sales tax follows the actual delivery address, and the ZIP is closer to that truth than a mistyped state field.
This applies whether the wrong state is in the address string or in the request path itself:
# Path says AL, ZIP is in MA → served MA, with disclosure.curl "https://api.taxql.com/v1/tax/al?zip=01773" -H "X-API-Key: $KEY"TaxQL only derives a state when the ZIP maps unambiguously to a single state; a rare ZIP that straddles a state line is left as-is.
Reading the disclosure
Section titled “Reading the disclosure”When anything was normalized, the response carries a normalization block
(under meta in the default simple shape, top-level in mode=full). It is
absent entirely when nothing was changed, so clean input is unaffected.
{ "rate": { "combined_rate": "0.06250", "...": "..." }, "meta": { "resolved_via": "static_statewide", "warnings": [ "The submitted state 'AL' contradicts ZIP 01773 (which is in MA); served MA — the ZIP is authoritative for the state. If the ZIP itself is wrong, the served rate is wrong: verify the ZIP." ], "normalization": { "normalized": true, "applied": [ { "rule_id": "state_derived_from_zip", "field": "state", "raw": "AL", "normalized": "MA", "confidence": "heuristic", "detail": { "submitted_state": "AL", "derived_state": "MA", "zip": "01773" } } ] } }}Each applied rule carries a stable rule_id, the field it touched, the raw
value received, the normalized value used, and a confidence of
unambiguous or heuristic. The same human-readable message is also appended
to meta.warnings.
Strict mode
Section titled “Strict mode”Pass ?strict=on (also true / 1) to turn every repair into a loud
rejection instead. Input that would be normalized returns 400 INVALID_INPUT
with a correction block you can act on programmatically:
{ "error": { "code": "INVALID_INPUT", "message": "The state field 'AL' does not match ZIP 01773, which is in MA. Both are reported so you can decide which field is wrong — the ZIP or the state.", "correction": { "field": "state", "raw": "AL", "suggested": "MA", "suggested_address": "32 Hillside Road, Lincoln, MA 01773", "rule_id": "state_derived_from_zip", "detail": { "submitted_state": "AL", "derived_state": "MA", "zip": "01773" }, "hint": "Correct the state to MA (or fix the ZIP if the state was right), or drop strict=on to have the ZIP's state used automatically with a disclosure." } }}strict value | behavior |
|---|---|
omitted, off, false, 0 | permissive — normalize and disclose (default) |
on, true, 1 | strict — reject normalizable input with a suggested correction |
| anything else | 400 listing the accepted values |
strict is accepted (and harmlessly ignored) in lat/lng mode, which has
nothing to normalize. Use strict mode when you’d rather your pipeline surface a
bad address than have TaxQL quietly correct it — for example, when you’re
auditing the quality of an upstream address feed.