Why Location Normalization Is Harder Than It Looks

CleanJobData Engineering

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:

  • NYC
  • New York, NY
  • San Francisco / Remote
  • Remote - United States
  • London, UK
  • CA
  • North 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:

  1. Structured provider data — some ATS platforms provide addresses or location arrays.
  2. Parsed location strings — free-text locations are split into candidate segments.
  3. Country hints — source context helps disambiguate ambiguous names.
  4. Geo resolution — city, state, and country IDs are resolved against the geographic database.
  5. Deduplication — repeated or overlapping rows are removed.
  6. Remote handling — remote jobs are marked with has_remote and 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.

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:

  • location as the original display string
  • locations as the structured normalized array
  • has_remote as 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.