Route each address to the geocoder that performs best in its jurisdiction by keying a routing table on the ISO 3166-1 alpha-2 country code, resolving an ordered provider chain with a DEFAULT fallback, and dispatching to the first entry. This page is part of the Dynamic Provider Selection Based on Region guide.
The Routing Table Pattern
The core artifact is a plain dictionary from country code to an ordered list of provider identifiers. First entry is the primary; the rest are fallbacks. A single DEFAULT key catches every code you have not explicitly tuned, so the dispatcher always resolves something.
ROUTING_TABLE: dict[str, tuple[str, ...]] = {
"US": ("google", "here", "nominatim"),
"GB": ("google", "os_places", "nominatim"),
"DE": ("here", "google", "nominatim"),
"AT": ("here", "google", "nominatim"),
"CH": ("here", "google", "nominatim"),
"JP": ("google", "yahoo_japan"),
"CN": ("baidu", "gaode"),
"AU": ("here", "google"),
"DEFAULT": ("nominatim", "google"),
}
Country codes are the right key because every provider documents coverage and pricing per country, and a code is cheap to derive from any parsed address. Subdivision-level routing (US-CA) is a natural extension but country granularity handles the overwhelming majority of production traffic.
Routing Table Structure
| Field | Type | Role | Example |
|---|---|---|---|
| Key | str (ISO 3166-1 alpha-2) |
Country the row applies to | "DE" |
| Value[0] | str |
Primary provider for that country | "here" |
| Value[1:] | str (ordered) |
Fallback providers, most-preferred first | ("google", "nominatim") |
DEFAULT key |
str |
Catch-all for unmapped countries | ("nominatim", "google") |
Keep the value an immutable tuple so a shared table cannot be mutated by a worker mid-run, and load it once at startup rather than rebuilding it per request.
Deriving the Country Code
Parsed components rarely arrive perfectly clean: some carry a full country name, some a lowercase code, some nothing. Normalize with a compile-once alias map and a regex that strips non-letters, defaulting to DEFAULT when no confident code emerges.
"""country_router.py — resolve a provider chain from a parsed address."""
from __future__ import annotations
import re
# Compile-once: strip anything that is not an ASCII letter.
_NON_ALPHA = re.compile(r"[^A-Za-z]")
# Map common full names / legacy codes to ISO 3166-1 alpha-2.
_NAME_TO_ISO: dict[str, str] = {
"USA": "US", "UNITEDSTATES": "US", "UK": "GB", "UNITEDKINGDOM": "GB",
"GREATBRITAIN": "GB", "DEUTSCHLAND": "DE", "GERMANY": "DE",
"JAPAN": "JP", "NIPPON": "JP", "CHINA": "CN",
}
ROUTING_TABLE: dict[str, tuple[str, ...]] = {
"US": ("google", "here", "nominatim"),
"GB": ("google", "os_places", "nominatim"),
"DE": ("here", "google", "nominatim"),
"JP": ("google", "yahoo_japan"),
"CN": ("baidu", "gaode"),
"DEFAULT": ("nominatim", "google"),
}
def derive_country_code(components: dict[str, str]) -> str:
"""Return an ISO 3166-1 alpha-2 code from parsed components, or 'DEFAULT'.
Args:
components: Parsed address fields, e.g. {'country_code': 'us'} or
{'country': 'United States'}.
Returns:
A two-letter uppercase ISO code known to the routing table, or
'DEFAULT' when no confident match is found.
"""
raw = components.get("country_code") or components.get("country") or ""
cleaned = _NON_ALPHA.sub("", raw).upper()
if len(cleaned) == 2 and cleaned in ROUTING_TABLE:
return cleaned
if cleaned in _NAME_TO_ISO:
return _NAME_TO_ISO[cleaned]
return "DEFAULT"
def resolve_chain(country_code: str) -> tuple[str, ...]:
"""Look up the ordered provider chain, defaulting when unmapped."""
return ROUTING_TABLE.get(country_code, ROUTING_TABLE["DEFAULT"])
Dispatching Along the Chain
The dispatcher walks the resolved chain and returns the first successful result, cascading on any provider error. This is the country-aware entry point into a broader fallback chain for failed lookups.
from typing import Any, Callable, Optional
# provider name -> callable(address) -> geocode dict, raising on failure
GeocodeFn = Callable[[str], dict[str, Any]]
def dispatch(
address: str,
components: dict[str, str],
providers: dict[str, GeocodeFn],
) -> Optional[dict[str, Any]]:
"""Route one address to the best provider for its country and geocode it.
Args:
address: Raw address string to geocode.
components: Parsed components used to derive the country code.
providers: Mapping of provider name to a geocoding callable.
Returns:
The first successful geocode result annotated with the provider
that produced it, or None if every provider in the chain failed.
"""
chain = resolve_chain(derive_country_code(components))
for name in chain:
fn = providers.get(name)
if fn is None:
continue
try:
result = fn(address)
except Exception:
# Provider failed; cascade to the next in the chain.
continue
result["_provider"] = name
return result
return None
Vectorized pandas variant
For bulk assignment, map the country-code column straight to a primary-provider column with .map, which is far faster than a row-wise .apply on large frames.
import pandas as pd
# Primary provider only (chain[0]) for a fast column assignment.
_PRIMARY: dict[str, str] = {code: chain[0] for code, chain in ROUTING_TABLE.items()}
_DEFAULT_PRIMARY = ROUTING_TABLE["DEFAULT"][0]
def assign_primary_provider(
df: pd.DataFrame, code_col: str = "country_code"
) -> pd.DataFrame:
"""Add a 'primary_provider' column derived from each row's country code.
Args:
df: Frame containing a normalized ISO country-code column.
code_col: Name of that column.
Returns:
A copy of df with a 'primary_provider' column.
"""
out = df.copy()
codes = out[code_col].astype(str).str.upper()
out["primary_provider"] = codes.map(_PRIMARY).fillna(_DEFAULT_PRIMARY)
return out
Edge Cases
Missing or ambiguous country field
An address with no country signal (a bare street line) yields an empty string, which derive_country_code maps to DEFAULT. That is safe but coarse — for higher accuracy, run the string through a locale-aware parser first so a real code is present before routing. Reliable code derivation depends on upstream international address format standardization.
Legacy or non-canonical codes
Inputs like UK (not the ISO GB) or USA (alpha-3) will not match a two-letter table key directly. The _NAME_TO_ISO alias map absorbs the common ones; extend it from your own data rather than assuming input is already canonical, and log any value that falls through to DEFAULT so you can spot systematic gaps.
A routed provider lacks legal coverage
Some countries restrict which providers may legally geocode their addresses (China is the canonical case). The routing table encodes this as data — "CN": ("baidu", "gaode") deliberately omits Western providers — so compliance lives in one auditable place instead of scattered if statements. Confirm each chain against provider terms; the accuracy trade-offs are covered under comparing geocoding accuracy across providers.
Integration Note
Country-code routing is the decision layer that sits above dispatch: it picks which provider chain to try, while the parent dynamic provider selection based on region guide adds the quota and health checks that decide whether the chosen provider is actually usable right now. Pair the table with an accuracy baseline from comparing geocoding accuracy across providers so each country’s ordering reflects measured precision rather than a guess, and feed misses into the fallback chain for failed lookups.
Related
- Dynamic Provider Selection Based on Region — the parent guide adding quota checks, health scoring, and async dispatch around this routing table.
- International Address Format Standardization — how to parse components reliably so the country code you route on is trustworthy.
- Comparing Geocoding Accuracy Across Providers — measure per-country precision to order each provider chain on evidence.
- Implementing Fallback Chains for Failed Lookups — cascade to the next provider in the chain when the routed primary fails.