Why Location Normalization Is Harder Than It Looks
TL;DR
Raw location strings from ATS sources are inconsistent free text — CleanJobData splits them into an original location display string and a structured locations array with resolved city/state/country IDs. Some strings are genuinely ambiguous (Georgia the state vs. the country, a bare 'CA'); those get a best-effort resolution flagged with ambiguity_reason, candidate_count, and a capped confidence score rather than a silent guess. Check those fields before trusting a row for anything compliance- or benchmarking-sensitive.
Location looks simple until you try to filter it. A human can understand SF, San Francisco, Remote US, London / New York, and California. A database cannot do that reliably unless the data has been normalized first.
CleanJobData turns messy location strings into structured location rows with city, state, and country IDs.
The Problem with Raw Strings
Raw location strings are inconsistent across sources:
NYCNew York, NYSan Francisco / RemoteRemote - United StatesLondon, UKCANorth America
String matching fails quickly. Searching for CA can match California, Canada, or unrelated text. Searching for Remote can accidentally include jobs that are only partially remote.
How Normalization Works
The backend combines multiple signals:
- Structured provider data — some ATS platforms provide addresses or location arrays.
- Parsed location strings — free-text locations are split into candidate segments.
- Country hints — source context helps disambiguate ambiguous names.
- Geo resolution — city, state, and country IDs are resolved against the geographic database.
- Deduplication — repeated or overlapping rows are removed.
- Remote handling — remote jobs are marked with
has_remoteand location rows can include remote context.
The result is a stable locations array that can be filtered, displayed, and indexed. For the exact fields, filter parameters, and code examples, see the Job Location Normalization guide.
Why It Matters for Product
Normalized locations power better product experiences:
- city-specific job boards
- state and country landing pages
- remote-only filters
- location chips
- map views
- analytics by region
- SEO pages that match user intent
Without normalization, every page becomes a string-matching problem. With normalized locations, filtering becomes a structured query.
Why It Matters for Search
Location normalization also improves search relevance. A job in San Francisco should not appear because the description mentions California, and a remote job should not be treated the same as an office job unless the filter explicitly asks for it.
That is why CleanJobData separates:
locationas the original display stringlocationsas the structured normalized arrayhas_remoteas the remote-work flag
What Still Can't Be Resolved with Certainty
Not every raw string carries enough information to disambiguate, and no amount of normalization logic closes that gap completely. Georgia is the clearest case — it's simultaneously a US state and a country, and if the employer didn't give any other clue (no office address, no country hint in the source data), the resolver has to make a best-effort call rather than a certain one. The same problem shows up one level down: a bare city name with no state or country attached can't always be pinned to the right place when multiple cities share that name, and a source that only gives "CA" is genuinely ambiguous between California and Canada without more context.
The resolver leans on whatever surrounding signals the source provides — office names, other locations on the same posting, country hints passed along by the ATS adapter — to break ties where it can, but when a posting genuinely doesn't include enough of that context, there's no way to reach 100% accuracy from the location string in isolation. Unlike a silent guess, though, a row that needed this kind of disambiguation is flagged in the response: it carries ambiguity_reason (why the match wasn't clean), candidate_count (how many places the raw string could have matched), and a confidence score capped well below a clean match's. A resolved row that came back clean has no ambiguity_reason and a confidence near 1; a forced best guess has both markers set.
If your product needs higher certainty than that for a specific case — a compliance-sensitive feature, or a market you're benchmarking closely — read confidence/ambiguity_reason on the row first; that's what they're there for. See the Job Location Normalization guide for the exact fields.
The Takeaway
Location normalization is hard because source data is inconsistent. CleanJobData solves it once in the backend so your frontend can filter by city, state, country, and remote status with confidence.
See it on real listings in the CleanJobData Playground, or read the Job Location Normalization guide for the full field reference.