> ## Documentation Index
> Fetch the complete documentation index at: https://jobo.world/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Resolve locations

> Turn messy free-text location strings from job postings and ATS systems into canonical city, region, country, and coordinates.

Job postings describe places inconsistently — `Store 04494 - Lynchburg, VA`, `münchen`, `US or Canada`, `2 Locations`. [`GET /api/locations/geocode`](/docs/api-reference/locations/geocode-a-location) resolves these to canonical labels and coordinates.

```bash theme={null}
curl "https://connect.jobo.world/api/locations/geocode?location=San+Francisco,+CA" \
  -H "X-Api-Key: $JOBO_API_KEY"
```

```python theme={null}
from jobo_enterprise import JoboClient

with JoboClient(api_key="YOUR_API_KEY") as client:
    result = client.locations.geocode("San Francisco, CA")

    for loc in result.locations:
        print(f"{loc.display_name}: {loc.latitude}, {loc.longitude}")
```

Geocode results are highly cacheable — the same input always resolves to the same output. Cache locally and you will rarely pay for a repeat lookup.

***

## How resolution works

Each input runs through a fixed sequence. Classification can short-circuit early; otherwise the pipeline extracts structured parts, geocodes each one, validates the result, and caches it.

```mermaid theme={null}
graph TD
    A["Input — raw location string"] --> B{"Step 1 — Classify"}
    B -->|Remote sentinel| R["Return — remote keyword"]
    B -->|Placeholder| P["Return — error"]
    B -->|Real location| N["Step 2 — Normalize"]
    N --> C{"Step 3 — Cache lookup"}
    C -->|Hit| Z["Return — cached result"]
    C -->|Miss| L["Step 4 — LLM extraction"]
    L -->|No parts| X["Return — no match"]
    L -->|Parts| G["Step 5 — Geocode each part"]
    G --> F{"Step 6 — Validation"}
    F -->|All rejected| X
    F -->|Accepted parts| W["Step 7 — Cache the result"]
    W --> Y["Return — resolved"]
```

<Steps>
  <Step title="Classify (remote / placeholder)">
    The trimmed input is checked against two regex sets before anything else runs.

    **Remote sentinels** return immediately as `remote_keyword`, with `country: "Remote"` and no coordinates: `remote`, `anywhere`, `global`, `worldwide`, `work from home`, `wfh`, `telecommute`, `distributed` — optionally followed by `/`, `|`, or `,` plus one of `traveling`, `travel`, `anywhere`, `remote`, `virtual`, or a trailing parenthetical.

    **Placeholders** return an error with `succeeded: false` and no geocoding attempted:

    * Counts — `2 Locations`, `15 locations`
    * Quantifiers — `multiple`/`various`/`several`/`many`/`all locations`
    * Single words — `hq`, `headquarters`, `head office`, `office`, `location`, `flexible`, `hybrid`, `onsite`, `on-site`, `various`, `n/a`, `tbd`, `tba`
  </Step>

  <Step title="Normalize">
    Real inputs are normalized into a stable cache key — abbreviations expanded, diacritics folded, casing standardized. Inputs differing only in case, punctuation, or common abbreviations share a cache entry.
  </Step>

  <Step title="Cache lookup">
    On hit, the previously resolved locations return immediately with `method: "cache"`.

    Only successful resolutions are cached. Failed lookups are not written, so the same input can succeed later as data quality improves.
  </Step>

  <Step title="LLM extraction">
    On cache miss, the raw input goes to a language model that handles translation, abbreviation expansion, multi-location splitting (`"US or Canada"` → two parts), and noise removal (`"Store 04494 - Lynchburg, VA"` → `Lynchburg / Virginia / United States`). It returns zero or more `{city, region, country}` parts.

    No usable parts means `no_match`.
  </Step>

  <Step title="Geocode each part">
    Each part goes to a structured geocoder returning canonical city / region / country / country-code labels plus coordinates. **The geocoder's labels are authoritative** — the saved location uses its casing and spelling, not the model's. Parts the geocoder cannot resolve are dropped.
  </Step>

  <Step title="Validation">
    Each geocoded result is validated against the original input by comparing token overlap with the canonical labels. This catches mis-geocodes (`Springfield, IL` resolving to Springfield, MA) and confabulations (inventing a country the input never mentioned).

    The match score (0.0–1.0) is returned as `fuzzy_confidence`. Results below the threshold are rejected. If every result is rejected, the response is `no_match`.
  </Step>

  <Step title="Cache the result">
    Accepted results are saved under the normalized key, so the next identical request is served instantly. The response returns with `method: "llm"`.
  </Step>
</Steps>

### The `method` field

Every response carries `method`, telling you which branch produced the result.

| `method`         | Meaning                                                                |
| ---------------- | ---------------------------------------------------------------------- |
| `cache`          | Step 3 hit — served from the cache.                                    |
| `llm`            | Steps 4–7 — extracted, geocoded, validated, and cached for next time.  |
| `remote_keyword` | Step 1 — input matched a remote sentinel (e.g. `Remote`, `Worldwide`). |
| `no_match`       | Step 4 returned no parts, or step 6 rejected every candidate.          |
| `error`          | Step 1 detected a placeholder or non-location (e.g. `TBD`, `Various`). |

<Info>
  Most repeated lookups hit the cache and return in single-digit milliseconds. Cache misses go through the full pipeline and typically complete in a few hundred milliseconds. The endpoint has a 15-second budget; on timeout it returns `200` with `succeeded: false` and `method: "timeout"`.
</Info>

***

## Supported input formats

Because extraction is model-driven there is no fixed format grammar. The table below shows formats observed working well in practice; it is not exhaustive.

| Format                             | Examples                                                                                                             |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **City, State (US)**               | `San Francisco, CA` · `New York, NY` · `Austin, Texas` · `Portland, OR` · `Denver, Colorado` · `Raleigh, NC`         |
| **City, Country**                  | `London, UK` · `Tokyo, Japan` · `Berlin, DE` · `Mumbai, India` · `Dubai, UAE` · `São Paulo, Brazil`                  |
| **City, Province (CA)**            | `Toronto, ON` · `Vancouver, BC` · `Montreal, QC` · `Calgary, AB` · `Ottawa, Ontario` · `Halifax, NS`                 |
| **City, State (AU)**               | `Sydney, NSW` · `Melbourne, VIC` · `Brisbane, QLD` · `Perth, WA`                                                     |
| **City, Constituent Country (UK)** | `London, England` · `Edinburgh, Scotland` · `Cardiff, Wales` · `Belfast, Northern Ireland`                           |
| **City, Bundesland, Germany**      | `Munich, Bavaria, Germany` · `Stuttgart, Baden-Württemberg`                                                          |
| **City, State, India**             | `Pune, Maharashtra, India` · `Bengaluru, Karnataka`                                                                  |
| **Country codes (ISO alpha-2)**    | `US` · `GB` · `DE` · `FR` · `JP` · `IN` · `AU` · `CA` · `BR` · `SG`                                                  |
| **Country codes (ISO alpha-3)**    | `DEU` · `GBR` · `ITA` · `SWE` · `AUS` · `NZL` · `JPN` · `CHN`                                                        |
| **Country-first formats**          | `Germany, Berlin` · `ITA, Rome` · `India, Mumbai`                                                                    |
| **Full country names**             | `United States` · `United Kingdom` · `Germany` · `Japan` · `South Korea`                                             |
| **Country name variants**          | `Deutschland` · `España` · `Schweiz` · `日本` · `भारत` · `Czechia` · `Türkiye`                                         |
| **International / diacritics**     | `München, Deutschland` · `São Paulo, Brazil` · `Zürich, Switzerland` · `Köln, DE` · `Malmö, Sweden`                  |
| **City aliases & abbreviations**   | `NYC` · `SF` · `LA` · `DC` · `CDMX` · `Bay Area` · `Silicon Valley` · `DFW` · `RTP`                                  |
| **City transliterations**          | `Bombay` (→ Mumbai) · `Peking` (→ Beijing) · `Calcutta` (→ Kolkata) · `Madras` (→ Chennai)                           |
| **Well-known cities (standalone)** | `London` · `Tokyo` · `Dubai` · `Singapore` · `Amsterdam` · `Dublin` · `Tel Aviv`                                     |
| **Compound locations**             | `US or Canada` · `New York and San Francisco` · `London & Paris` · `EMEA or APAC`                                    |
| **Remote keywords**                | `Remote` · `Anywhere` · `Global` · `Worldwide` · `Work from Home` · `WFH` · `Telecommute` · `Distributed`            |
| **Region aliases**                 | `EMEA` · `APAC` · `LATAM` · `DACH` · `BENELUX` · `NORDICS` · `ANZ` · `GCC` · `MENA`                                  |
| **Ambiguous codes**                | `CA` (California or Canada) · `IN` (Indiana or India) · `DE` (Delaware or Germany) · `GA` (Georgia state or country) |

### Compound locations

Compound expressions split on `or`, `and`, `&`, `;`, and `|`. Splitting happens during extraction, which returns one part per location. Each part is geocoded independently, and the resulting array comes back in `locations` on a single response.

```bash theme={null}
curl "https://connect.jobo.world/api/locations/geocode?location=US+or+Canada" \
  -H "X-Api-Key: $JOBO_API_KEY"
```

```json theme={null}
{
  "input": "US or Canada",
  "succeeded": true,
  "locations": [
    {
      "city": null,
      "region": null,
      "country": "United States",
      "country_code": "US",
      "latitude": 39.8283,
      "longitude": -98.5795,
      "display_name": "United States",
      "fuzzy_confidence": 1.0
    },
    {
      "city": null,
      "region": null,
      "country": "Canada",
      "country_code": "CA",
      "latitude": 56.1304,
      "longitude": -106.3468,
      "display_name": "Canada",
      "fuzzy_confidence": 1.0
    }
  ],
  "method": "llm"
}
```

| Input                         | Resolved parts                |
| ----------------------------- | ----------------------------- |
| `US or Canada`                | `US`, `Canada`                |
| `New York and San Francisco`  | `New York`, `San Francisco`   |
| `London & Paris`              | `London`, `Paris`             |
| `EMEA or APAC`                | `EMEA`, `APAC`                |
| `Remote or San Francisco, CA` | `Remote`, `San Francisco, CA` |

### Ambiguous codes

Some two-letter codes match both a US state abbreviation and an ISO country code. Disambiguation uses surrounding city or context tokens.

| Code | US State                | Country           |
| ---- | ----------------------- | ----------------- |
| `CA` | California              | Canada            |
| `IN` | Indiana                 | India             |
| `DE` | Delaware                | Germany           |
| `GA` | Georgia (state)         | Georgia (country) |
| `CO` | Colorado                | Colombia          |
| `AL` | Alabama                 | Albania           |
| `ME` | Maine                   | Montenegro        |
| `NL` | Newfoundland & Labrador | Netherlands       |
| `SK` | Saskatchewan            | Slovakia          |

Heuristics the resolver tends to apply:

1. **A standalone two-letter code** usually resolves to the more common US state reading — `CA` alone → California.
2. **A code paired with a city** is disambiguated by city context — `Los Angeles, CA` → California; `Toronto, CA` → Canada; `Amsterdam, NL` → Netherlands; `Tel Aviv, IL` → Israel.
3. **Three-letter codes** are unambiguous ISO alpha-3 — `DEU` → Germany; `GBR` → United Kingdom.

Validation compares canonical labels back against the original input, so if the resolver settles on `Canada` for an input containing `California`, it is typically rejected.

<Info>
  For best results with ambiguous codes, include a city name or use the full state/country name — prefer `California` or `Canada` over a bare `CA`.
</Info>

***

## Tips

<CardGroup cols={2}>
  <Card title="Be specific" icon="bullseye">
    Include both city and state/country when possible. `San Francisco, CA` beats a bare `CA`.
  </Card>

  <Card title="Spell out ambiguous codes" icon="spell-check">
    Use `California` or `Canada` instead of `CA`; `Germany` instead of `DE`.
  </Card>

  <Card title="Use standard separators" icon="split">
    `or`, `and`, `&`, `;`, or `|` between locations. `US or Canada` works; `US, Canada` may be read as one location.
  </Card>

  <Card title="Ignore case and diacritics" icon="text-size">
    `münchen`, `MÜNCHEN`, and `Munchen` all resolve to Munich, Germany.
  </Card>

  <Card title="Aliases are handled" icon="city">
    `NYC`, `SF`, `LA`, `DC` and transliterations like `Bombay` or `Peking` resolve automatically.
  </Card>

  <Card title="Noise is stripped" icon="broom">
    `Store 04494 - Lynchburg, VA` and `Greater San Francisco Area` normalize to the underlying city.
  </Card>
</CardGroup>

***

## Unresolvable input

A location that cannot be resolved is **not** an error. The endpoint returns `200` with `succeeded: false`, so check that field rather than the status code.

```json theme={null}
{
  "input": "2 Locations",
  "succeeded": false,
  "error": "Location string is a placeholder, not a real location."
}
```

```json theme={null}
{
  "input": "xyzzy-unknown-place-12345",
  "succeeded": false,
  "method": "no_match",
  "error": "Could not resolve the provided location."
}
```

A missing or whitespace-only `location` parameter is a genuine `400`. See [Errors](/docs/errors) for the envelope.
