Addresses arrive in two ways. Either your form has separate fields for street, city, state and ZIP, and people still paste the whole thing into the first one, or you import addresses from somewhere else, a CSV from a trade show or a legacy CRM, where they were always one string. Both end in the same place: 350 5th Ave Apt 21B, New York, NY 10118 and 350 Fifth Avenue #21B New York 10118 are the same apartment, and nothing in your database knows it.
This lesson builds the three steps that fix that: parse the string into parts, check the parts against reference data, and assemble a normalised key. It uses Street Address Parser and Zip Codes Lookup, both 2 credits a call, with Cities Lookup for the cases where all you have is a city name.
One limit up front, because it shapes everything else: these tools parse and cross-check. They do not certify that a mailbox exists at the address. That needs postal-authority data such as USPS address certification, and it is a different, more expensive product. What you get here is cheap structure, which is what dedupe and most shipping forms actually need.
What the parser returns
Start with a clean US address:
curl "https://api.apiverve.com/v1/streetaddressparser?address=1600+Amphitheatre+Parkway%2C+Mountain+View%2C+CA+94043" \
-H "x-api-key: YOUR_API_KEY"streetAddress is the street NAME (Amphitheatre); the number and the type are separate fields.
The documented fields sit under data.parsed: streetNumber, streetAddress, streetType, cityName, stateName and zipCode. Two of them are named in ways that cause bugs. streetAddress is the street name alone, Amphitheatre, not the full street line. stateName is the two-letter abbreviation, CA, not California.
The parser does real normalisation work on the street type and directionals. Parkway, parkway and Pkwy. all come back as Pkwy; street comes back St; Terrace comes back Ter; and the word north in 123 north main street comes back as streetPrefix: "N". A ZIP+4 such as 94043-1351 is cut to zipCode: "94043". It does not change the case of names, though: springfield il stays springfield and il. You will uppercase those yourself.
Unit numbers come back in fields that the published definition does not list yet, so code defensively around them. Observed in real responses:
{
"sec_unit_type": "Apt",
"sec_unit_num": "21B",
"streetNumber": "350",
"streetType": "Ave",
"streetAddress": "5th",
"cityName": "New York",
"stateName": "NY",
"zipCode": "10118"
}
#2 comes back as sec_unit_type: "#" and sec_unit_num: "2"; suite 400 as suite and 400. Parts that are not present are omitted from parsed, not set to null. Parse just 1600 amphitheatre parkway and you get three keys and no city at all. Read every field with a default.
The parser returns streetNumber 1600, streetAddress Amphitheatre, streetType Pkwy. What should you print on the first line of a shipping label?
Answer the check above to continue.
Build a normalised address key
With parsed, checked parts you can assemble a key that two spellings of the same address share. Uppercase everything, drop punctuation, collapse spaces, and use the parser's abbreviations, which are already consistent.
const clean = (s) => (s ?? '').toUpperCase().replace(/[^A-Z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
export function addressKey(p) {
const unit = p.sec_unit_num ? `#${clean(p.sec_unit_num)}` : '';
return [p.streetNumber, p.streetPrefix, p.streetAddress, p.streetType, unit, p.zipCode]
.map((part) => (part === unit ? part : clean(part)))
.filter(Boolean)
.join(' ');
}
export async function parseAddress(address) {
const res = await fetch(`https://api.apiverve.com/v1/streetaddressparser?${new URLSearchParams({ address })}`, {
headers: { 'x-api-key': process.env.APIVERVE_API_KEY },
signal: AbortSignal.timeout(3000),
});
const body = await res.json();
if (!res.ok || body.status !== 'ok') throw new Error(body.error || `HTTP ${res.status}`);
return body.data.parsed;
}import os
import re
import requests
def clean(s) -> str:
s = re.sub(r"[^A-Z0-9 ]", "", (s or "").upper())
return re.sub(r"\s+", " ", s).strip()
def address_key(p: dict) -> str:
unit = f"#{clean(p.get('sec_unit_num'))}" if p.get("sec_unit_num") else ""
parts = [clean(p.get("streetNumber")), clean(p.get("streetPrefix")), clean(p.get("streetAddress")),
clean(p.get("streetType")), unit, clean(p.get("zipCode"))]
return " ".join(x for x in parts if x)
def parse_address(address: str) -> dict:
r = requests.get("https://api.apiverve.com/v1/streetaddressparser",
params={"address": address},
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]}, timeout=3)
r.raise_for_status()
return r.json()["data"]["parsed"]1600 Amphitheatre Parkway, Mountain View, CA 94043 and 1600 amphitheatre parkway, mountain view, ca 94043-1351 both produce 1600 AMPHITHEATRE PKWY 94043. The key leaves out city and state on purpose: the ZIP already implies them, and city spellings are the noisiest part of any address. The unit is kept, because two flats in the same building are two customers.
Where the key will not help: 5th against Fifth, and street names that people abbreviate in their own ways (Saint and St, Mount and Mt). The parser normalises street types, not words inside street names. If those matter in your data, add a small replacement table for numbered and saint names before hashing, and accept that some duplicates will need a human.
For a backlog import, run the parser over every row once, store the parsed parts and the key next to the original string, and keep the original. You will want it the first time someone asks why two addresses were merged. Batch requests let you parse up to 200 addresses per call; see batch requests.
Next
You now have structured, cross-checked addresses and a dedupe key. The last lesson handles the fields that look easiest and cause the most embarrassment: names and usernames. Continue to Names, gibberish and junk input.