How to Parse US Addresses in Go
Turn a freeform address string into clean, column-ready fields. Free API, a small net/http client with typed structs, and a clear map from every component to your database.
You have a column called address full of freeform strings: "1600 Pennsylvania Ave NW Apt 4B, Washington DC 20500". It is fine for display, but useless for anything structured - you can't group by city, filter by ZIP, sort by street, or de-duplicate, because every part of the address is mashed into one field.
Address parsing fixes that. You send the raw string and get back discrete components: the primary number, the street name, the suffix, the directional, the unit, the city, the state, and the ZIP - each in its own field, ready to drop into its own column.
This tutorial shows you how to parse US addresses in Go using sthan.io's address API. We use only the standard library - net/http and encoding/json - so the client drops into an HTTP service, a CLI, a worker, or a one-off migration unchanged, with no third-party dependencies.
Quick summary: Send your API key as aBearertoken, callGET /v2/address-parser/usa/{address}, and decode the components from theResultfield -addressNumber,streetName,streetPostType,unitType,unitNumber,city,stateCode,zipCode. The free tier gives you 100 lookups/month - no credit card required.
What you'll need: Go 1.18+ and a free sthan.io account. No credit card, no approval queue. The free parser tier is 100 lookups/month; paid plans start at $8/month if you need more.
Try it first
Parse any US address right here - no signup required:
Try it live
That's what you're building. Type a messy one-line address and the API hands back every component as a separate, standardized field.
What the API returns
Every response is wrapped in a standard envelope. For parsing, the Result field is a single object whose properties are the address components. This is a real response for 1600 Pennsylvania Ave NW Apt 4B:
{
"Id": "2737f8f3-af83-4ba1-b9c3-e29d2ba9e03b",
"Result": {
"inputAddress": "1600 Pennsylvania Ave NW Apt 4B Washington DC 20500",
"addressLine1": "1600 PENNSYLVANIA AVE NW",
"addressLine2": null,
"addressNumber": "1600",
"streetPreDir": "",
"streetName": "PENNSYLVANIA",
"streetPostType": "AVE",
"streetPostDir": "NW",
"unitType": "apt",
"unitNumber": "4b",
"city": "WASHINGTON",
"stateCode": "DC",
"zipCode": "20500",
"zip4": null,
"county": null,
"matchMode": "Speculative",
"matchTier": "Near",
"confidence": 0.7,
"matchCode": {
"houseNumber": "Matched",
"street": "Matched",
"unit": "Matched",
"city": "Matched",
"state": "Matched",
"zipCode": "Matched",
"zip4": "NotApplicable"
},
"footnotes": ["recovered: standardized via correction, not an exact match"]
},
"ClientSessionId": null,
"StatusCode": 200,
"IsError": false,
"Errors": []
}
Id, Result, StatusCode, IsError, Errors) are PascalCase, while the component fields inside Result are camelCase (addressNumber, streetName, streetPostType). Go's encoding/json matches struct fields case-insensitively, but it's safest to pin the exact JSON name with a json:"..." tag on every field - which is what the structs below do.
The fields you'll use most often are covered in the component map below. Note that some fields are empty or null when they don't apply - there's no leading directional here, so streetPreDir is an empty string, and the parser didn't append a zip4 or county, so those are null.
Get your API key
- Sign up at sthan.io and subscribe to the free Address Parser tier
- Open your dashboard and create an API key
- Copy the key - it looks like
sthan_live_xxxxxxxxxxxxxxxx
You get the key immediately, with no approval queue. An API key is the simplest way to authenticate: you send it as a Bearer token on every request and there is no separate login step. (If you prefer a short-lived token, there is a JWT flow covered later.)
Configure the project
No dependencies to install - everything is in the standard library. Keep your key out of source control by reading it from the environment:
# Set the key in your shell (or your process manager / container env)
export STHAN_API_KEY="sthan_live_xxxxxxxxxxxxxxxx"
.env file locally - and add .env to .gitignore.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
)
var (
sthanBaseURL = "https://api.sthan.io"
sthanAPIKey = os.Getenv("STHAN_API_KEY") // read once at startup
)
Build the parser client
Define two structs that mirror the response. The envelope tags are PascalCase; the component tags are camelCase - exactly matching the JSON. Then use a single shared http.Client, call the endpoint, decode the envelope, and return the components:
// Envelope is the standard response wrapper (PascalCase keys).
type Envelope struct {
ID string `json:"Id"`
Result ParsedAddress `json:"Result"`
IsError bool `json:"IsError"`
Errors []string `json:"Errors"`
}
// ParsedAddress holds the address components (camelCase keys).
type ParsedAddress struct {
InputAddress string `json:"inputAddress"`
AddressNumber string `json:"addressNumber"`
StreetPreDir string `json:"streetPreDir"`
StreetName string `json:"streetName"`
StreetPostType string `json:"streetPostType"`
StreetPostDir string `json:"streetPostDir"`
UnitType string `json:"unitType"`
UnitNumber string `json:"unitNumber"`
City string `json:"city"`
StateCode string `json:"stateCode"`
ZipCode string `json:"zipCode"`
Zip4 string `json:"zip4"`
County string `json:"county"`
MatchTier string `json:"matchTier"`
Confidence float64 `json:"confidence"`
}
// One shared client: it pools connections and is safe for concurrent use.
var httpClient = &http.Client{Timeout: 10 * time.Second}
func ParseAddress(address, mode string) (*ParsedAddress, error) {
endpoint := fmt.Sprintf("%s/v2/address-parser/usa/%s?match=%s",
sthanBaseURL, url.PathEscape(address), mode)
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+sthanAPIKey)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, &APIError{StatusCode: resp.StatusCode}
}
var env Envelope
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
return nil, err
}
if env.IsError {
return nil, fmt.Errorf("parser error: %v", env.Errors)
}
return &env.Result, nil
}
That's the whole integration. One call:
result, err := ParseAddress(
"1600 pennsylvania ave nw apt 4b washington dc 20500", "speculative")
if err != nil {
log.Fatal(err)
}
fmt.Println(result.AddressNumber) // 1600
fmt.Println(result.StreetName) // PENNSYLVANIA
fmt.Println(result.StreetPostType) // AVE
fmt.Println(result.UnitType, result.UnitNumber) // apt 4b
fmt.Println(result.City, result.StateCode, result.ZipCode) // WASHINGTON DC 20500
http.Client? Creating a new client per call defeats connection reuse and can exhaust sockets under load. A single package-level http.Client is safe for concurrent use, pools connections between lookups, and lets you set one timeout - which matters as soon as you parse more than a handful of addresses, for example when backfilling an existing table.
Choose a match mode
The match parameter controls how much typo tolerance the parser applies while standardizing components. The same call supports four modes, from strictest to loosest:
| Mode | Behavior | Use when |
|---|---|---|
strict |
Only confident, exact-component matches; returns little or nothing when the input is ambiguous. | You only want components you can fully trust. |
balanced |
Exact plus typo-corrected components. Returns the best parse, flagging corrected fields. | Typical cleanup of user-entered addresses. |
fuzzy |
Wider recovery for messy or partial input. Higher recall, more corrections. | Backfilling a column of inconsistent legacy data. |
speculative |
Loosest recovery, with extra tolerance for heavily misspelled street names. Best-effort parses are flagged matchTier = "Speculative". |
Maximum recovery / agent tooling. This is the default. |
If you omit match, the endpoint defaults to speculative for the widest recovery. Whichever mode you pick, the location-defining parts of the address - the primary number, ordinal, directional, state, and the street's core name - are never substituted. A looser mode only widens tolerance for misspellings of the same street.
Map the components to your fields
Each component comes back as its own field, so mapping them to database columns is a direct copy. The fields you'll use most:
| Field | Meaning | Example |
|---|---|---|
addressNumber | Primary (house/building) number | 1600 |
streetPreDir | Leading directional | N in "N Main St" |
streetName | Core street name | PENNSYLVANIA |
streetPostType | Street suffix / type | AVE, ST, BLVD |
streetPostDir | Trailing directional | NW |
unitType / unitNumber | Secondary unit designator and value | apt / 4b |
city, stateCode, zipCode, zip4 | City, two-letter state, 5-digit ZIP, +4 | WASHINGTON, DC, 20500 |
A few more components cover edge cases: streetPreType, streetPreMod, streetPreSep, and streetPostMod capture pre/post modifiers in unusual street names (for example "Avenue of the Americas"). They're empty for ordinary addresses; add them to the struct only if you need them. Pair the parse with matchCode - a per-component breakdown (Matched / Corrected / Inferred / Unmatched / NotApplicable) - so you can tell which fields were trusted as-is versus corrected. Here is a parse going straight into a struct that's ready to persist:
type AddressRecord struct {
AddressNumber string
StreetPreDir string
StreetName string
StreetType string
StreetPostDir string
UnitType string
UnitNumber string
City string
State string
ZipCode string
Zip4 string
}
result, err := ParseAddress(rawAddress, "speculative")
if err != nil {
return err
}
record := AddressRecord{
AddressNumber: result.AddressNumber,
StreetPreDir: result.StreetPreDir,
StreetName: result.StreetName,
StreetType: result.StreetPostType,
StreetPostDir: result.StreetPostDir,
UnitType: result.UnitType,
UnitNumber: result.UnitNumber,
City: result.City,
State: result.StateCode,
ZipCode: result.ZipCode,
Zip4: result.Zip4,
}
// record is now ready to INSERT — one column per component
Alternative: JWT authentication
An API key is the simplest option and is all most apps need. If your security policy prefers short-lived credentials, the platform also supports a 2-step JWT flow. You call GET /Auth/Token once with your profileName and profilePassword headers, receive a token valid for up to 60 minutes, then send that token as the Bearer value on subsequent calls:
type tokenEnvelope struct {
Result struct {
AccessToken string `json:"access_token"`
} `json:"Result"`
}
func GetToken() (string, error) {
req, err := http.NewRequest(http.MethodGet, sthanBaseURL+"/Auth/Token", nil)
if err != nil {
return "", err
}
req.Header.Set("profileName", os.Getenv("STHAN_PROFILE_NAME"))
req.Header.Set("profilePassword", os.Getenv("STHAN_PROFILE_PASSWORD"))
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var env tokenEnvelope
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
return "", err
}
return env.Result.AccessToken, nil
}
You would then set Authorization: "Bearer "+token per request instead of the static key. Everything else - the endpoint, the envelope, the decoding - stays exactly the same. Cache the token and refresh it shortly before the 60-minute expiry rather than fetching one per request.
Handle errors
Two status codes are worth handling explicitly so a hiccup never stalls a batch job:
- 401 - The key or token was rejected. Check the value and, on the JWT flow, refresh and retry once.
- 429 - Rate limit reached. Back off and retry rather than dropping the row.
A small typed error carries the status code so the caller can branch on it:
type APIError struct{ StatusCode int }
func (e *APIError) Error() string {
return fmt.Sprintf("parser returned HTTP %d", e.StatusCode)
}
func ParseWithRetry(address, mode string, retries int) (*ParsedAddress, error) {
for attempt := 0; ; attempt++ {
result, err := ParseAddress(address, mode)
if err == nil {
return result, nil
}
var apiErr *APIError
if attempt < retries && errors.As(err, &apiErr) && apiErr.StatusCode == 429 {
time.Sleep(time.Duration(1<<attempt) * time.Second) // 1s, then 2s
continue
}
return nil, err
}
}
The exponential back-off (1s, then 2s) is enough for transient limits. For a large backfill, add a small delay between rows and a circuit breaker so one bad minute doesn't stall the whole queue. (Add "errors" to your imports for errors.As.)
What's next: confirm the parsed address is deliverable
Parsing gives you clean, structured components fast. It does not, on its own, confirm that mail or a package will actually arrive - a well-formed address can still point at a unit that no longer accepts delivery. When deliverability matters, run the address through the Address Verification API, which returns a deliverability status and appends ZIP+4 and county. It's the same envelope and the same net/http pattern - one GET against /v2/address-verification/usa/{address}. The Go walkthrough is here: Verify US Addresses in Go.
Prefer Python for this kind of work? The same parser, end to end, is covered in Parse US Addresses in Python. You can also explore the Address Parser product page for live code examples in more languages.
Frequently Asked Questions
Send your sthan.io API key as a Bearer token and call GET /v2/address-parser/usa/{address} with net/http. Decode the JSON into typed structs and read the components from the Result field - AddressNumber, StreetName, StreetPostType, UnitType, UnitNumber, City, StateCode, and ZipCode. The full working client is in the sections above.
The free tier includes 100 lookups per month with no credit card required. Paid plans start at $8/month. There is no trial period; the free tier is permanent. See pricing for higher-volume plans.
The parser breaks a freeform address into discrete fields: addressNumber, streetPreDir and streetPostDir (leading and trailing directionals), streetName, streetPostType (the suffix like Ave or St), unitType and unitNumber, city, stateCode, zipCode, and zip4. Each field is returned separately so you can store it in its own column.
Parsing splits a freeform address into structured components and standardizes their format. Verification goes a step further and confirms the address is real and deliverable, returning a deliverability status. Parse when you need clean, column-ready fields; verify when you need to know whether mail or a package will actually arrive. See Verify US Addresses in Go.
Call it from your Go backend, not the browser. The API does not enable CORS for browser requests, and putting your API key in client-side JavaScript would expose it to anyone viewing the page source. Parse server-side, then return the structured fields to the page.
Turn messy address strings into clean fields
Parse freeform addresses into structured components with one call - free tier of 100 lookups/month, paid from $8/month, no credit card to start.