Use HERE when your logistics pipeline demands deterministic address normalization, global postal compliance, and high-throughput asynchronous batch geocoding with explicit match-level scoring. Use Mapbox when you need rapid iteration, seamless web or mobile routing integration, and flexible semantic search for last-mile consumer delivery. This page is part of the Comparing Geocoding Accuracy Across Providers cluster.
Provider Decision Matrix
The table below summarises the three axes that matter most for logistics architecture decisions. When designing multi-API routing and fallback chains for enterprise dispatch, these are the precise properties that determine which provider sits in the primary slot.
| Axis | HERE Geocoding & Search API | Mapbox Geocoding API |
|---|---|---|
| Schema rigidity | Structured resultType + matchLevel + scoring.queryScore |
GeoJSON place_type[] array + relevance float |
| Postal standard | ISO 19160-1 compliant; quarterly postal DB updates | OpenStreetMap-based; frequent POI/road updates |
| Batch support | Native async batch endpoint (up to 10 000 addresses/job) | No native batch endpoint; 600 RPM synchronous cap |
| Routing graph | Shared spatial graph with HERE Routing API | Separate tile pipeline; may require coordinate snapping |
| Coordinate format | position.lat / position.lng discrete floats |
GeoJSON [longitude, latitude] array |
| Best fit | High-volume B2B freight, cross-border shipping | Last-mile consumer apps, real-time driver interfaces |
Architecture Overview
The diagram below shows how the two providers slot into a unified logistics normalization pipeline. Inputs are pre-cleaned and routed to the primary provider; low-confidence results fall through to the secondary provider before entering the dispatch store.
Core Technical Differentiators
Address Schema and Match Levels
HERE’s Geocoding & Search API returns a structured resultType field — valid values are houseNumber, place, street, postalCode, intersection, and locality — alongside a scoring.queryScore float and explicit matchLevel tags. This maps cleanly to logistics dispatch rules: a houseNumber result type triggers automated routing, while street or postalCode flags records for manual verification or fuzzy matching against historical delivery logs.
Mapbox’s Geocoding API returns a relevance score from 0 to 1 and a place_type array (e.g. ["address"], ["place"], ["region"]) alongside a context array for the administrative hierarchy. For cross-border freight, HERE’s strict adherence to ISO 19160-1 postal standards reduces normalization drift across regional addressing conventions. Before committing to a confidence threshold, establish your baseline precision using the methodology described under comparing geocoding accuracy across providers.
Batch Processing and Rate Limits
HERE supports synchronous single-address queries and asynchronous batch jobs via its dedicated Batch Geocoding endpoint, processing up to 10,000 addresses per job. Batch jobs return a job ID and require polling or webhook callbacks; this async model simplifies high-volume ETL workflows and is the primary reason logistics platforms choose HERE for bulk normalization.
Mapbox caps synchronous requests at 600 RPM on standard plans and offers no native batch endpoint — client-side chunking or external queue management is required. Logistics platforms processing large manifests typically route HERE for bulk normalization and reserve Mapbox for real-time driver-app lookups. When designing the queue architecture, applying rate-limiting strategies for batch processing keeps both providers within their quota bounds.
Routing Graph Alignment
HERE’s routing engine shares the same underlying spatial graph as its geocoder. Normalized addresses resolve directly to route network nodes without reprojection drift, which is critical for fleet dispatch accuracy.
Mapbox’s geocoder and Directions API use separate tile pipelines. Address-to-route transitions can require coordinate snapping or buffer validation. Data freshness SLAs also diverge: HERE pushes quarterly postal database updates with enterprise-grade change logs, while Mapbox syncs frequently with OpenStreetMap, prioritising POI and road network agility over strict postal compliance. Align your cache TTLs and fallback triggers with these update cycles.
API Parameter Reference
| Parameter | HERE | Mapbox |
|---|---|---|
| Query field | q (free-form string) |
Path-encoded address string |
| Result limit | limit (int) |
limit (int, max 10) |
| Country filter | in=countryCode:DEU,GBR |
country=de,gb |
| Language | lang=en-US |
language=en |
| Bounding box | in=bbox:minLon,minLat,maxLon,maxLat |
bbox=minLon,minLat,maxLon,maxLat |
| Result types | resultTypes=houseNumber,street |
No equivalent filter |
| Auth | apiKey query param |
access_token query param |
Note: Mapbox now recommends its v6 Geocoding API for new integrations. The v5 mapbox.places endpoint used in the implementation below remains supported for existing pipelines but check the Mapbox changelog before starting a new project.
Minimal Runnable Python Implementation
The implementation below compiles a unified NormalizedAddress model, parses both provider responses into the same schema, and includes a vectorized pandas variant for batch manifest processing.
from __future__ import annotations
import hashlib
import logging
import urllib.parse
from typing import Optional
import requests
from pydantic import BaseModel, Field
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
class NormalizedAddress(BaseModel):
"""Unified geocoding output schema for logistics dispatch."""
lat: float
lon: float
# HERE: resultType string; Mapbox: first element of place_type array
match_level: str
# HERE: scoring.queryScore float; Mapbox: relevance float (0–1)
confidence: float
raw_provider: str
normalized_street: Optional[str] = None
postal_code: Optional[str] = None
raw_input: str
cache_key: str = Field(default="")
@property
def is_routable(self) -> bool:
"""True when the result meets dispatch-quality thresholds."""
if self.raw_provider == "here":
return self.match_level == "houseNumber" and self.confidence >= 0.8
return self.match_level == "address" and self.confidence >= 0.85
def _cache_key(address: str) -> str:
return hashlib.sha256(address.strip().lower().encode()).hexdigest()
def parse_here_response(address: str, payload: dict) -> NormalizedAddress:
"""Parse HERE Geocoding & Search API v1 response into NormalizedAddress."""
items = payload.get("items", [])
if not items:
raise ValueError("HERE returned no results")
item = items[0]
position = item.get("position", {})
address_obj = item.get("address", {})
scoring = item.get("scoring", {})
return NormalizedAddress(
lat=float(position.get("lat", 0.0)),
lon=float(position.get("lng", 0.0)),
match_level=item.get("resultType", "unknown"),
confidence=float(scoring.get("queryScore", 0.0)),
raw_provider="here",
normalized_street=address_obj.get("street"),
postal_code=address_obj.get("postalCode"),
raw_input=address,
cache_key=_cache_key(address),
)
def parse_mapbox_response(address: str, payload: dict) -> NormalizedAddress:
"""Parse Mapbox Geocoding API v5 response into NormalizedAddress.
Note: Mapbox GeoJSON coordinates are [longitude, latitude] — not [lat, lon].
"""
features = payload.get("features", [])
if not features:
raise ValueError("Mapbox returned no results")
feature = features[0]
# Mapbox coordinates: [longitude, latitude]
coords = feature.get("geometry", {}).get("coordinates", [0.0, 0.0])
context = {
ctx["id"].split(".")[0]: ctx["text"]
for ctx in feature.get("context", [])
}
place_types: list[str] = feature.get("place_type", ["unknown"])
return NormalizedAddress(
lat=float(coords[1]), # latitude is second element
lon=float(coords[0]), # longitude is first element
match_level=place_types[0],
confidence=float(feature.get("relevance", 0.0)),
raw_provider="mapbox",
normalized_street=feature.get("place_name", ""),
postal_code=context.get("postcode", ""),
raw_input=address,
cache_key=_cache_key(address),
)
def geocode(address: str, provider: str, api_key: str) -> NormalizedAddress:
"""Route a single address to HERE or Mapbox and return a NormalizedAddress."""
headers = {"Accept": "application/json"}
if provider == "here":
url = "https://geocode.search.hereapi.com/v1/geocode"
params = {"q": address, "apiKey": api_key, "limit": 1}
resp = requests.get(url, params=params, headers=headers, timeout=10)
resp.raise_for_status()
return parse_here_response(address, resp.json())
if provider == "mapbox":
encoded = urllib.parse.quote(address)
url = f"https://api.mapbox.com/geocoding/v5/mapbox.places/{encoded}.json"
params = {"access_token": api_key, "limit": 1}
resp = requests.get(url, params=params, headers=headers, timeout=10)
resp.raise_for_status()
return parse_mapbox_response(address, resp.json())
raise ValueError(f"Unsupported provider: {provider!r}")
# Vectorized variant for pandas DataFrames
def geocode_series(
df: "pd.DataFrame",
address_col: str,
provider: str,
api_key: str,
) -> "pd.DataFrame":
"""Apply geocode() to every row of a DataFrame and expand into columns.
Returns the original DataFrame with lat, lon, match_level, confidence,
and cache_key columns appended.
"""
import pandas as pd # noqa: F401 — import deferred to avoid hard dependency
results = df[address_col].map(
lambda addr: geocode(addr, provider, api_key).model_dump()
)
return df.join(pd.json_normalize(results))
Edge Cases and Failure Modes
Swapped Coordinate Order
Mapbox returns GeoJSON [longitude, latitude] while HERE returns discrete position.lat / position.lng floats. Swapping the coordinate pair silently places delivery stops in the wrong hemisphere — and the error may not surface until a driver is routed to an ocean coordinate. The parse_mapbox_response function above enforces lat=coords[1], lon=coords[0] explicitly. Add an assertion guard to your ingestion layer:
assert -90 <= result.lat <= 90, f"lat out of range: {result.lat}"
assert -180 <= result.lon <= 180, f"lon out of range: {result.lon}"
HERE Batch Job Polling Timeout
HERE’s async batch endpoint returns a job ID immediately. If your polling loop has no maximum wait, a stuck job blocks the entire manifest. Implement exponential backoff with a hard timeout:
import time
def poll_here_batch(job_id: str, api_key: str, max_wait: int = 300) -> dict:
"""Poll a HERE batch geocoding job until completion or timeout."""
url = f"https://batch.geocoder.ls.hereapi.com/6.2/jobs/{job_id}"
params = {"apiKey": api_key, "action": "status"}
deadline = time.monotonic() + max_wait
delay = 2.0
while time.monotonic() < deadline:
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
status = data.get("Response", {}).get("Status", "")
if status == "completed":
return data
if status in ("failed", "deleted"):
raise RuntimeError(f"HERE batch job {job_id} ended with status {status!r}")
time.sleep(delay)
delay = min(delay * 1.5, 30.0)
raise TimeoutError(f"HERE batch job {job_id} did not complete within {max_wait}s")
Mapbox Relevance Score at Country Borders
Mapbox’s relevance score drops near country borders where OSM coverage is uneven. A score of 0.84 for an address in a border region may reflect a genuine partial match rather than a missing house number. Cross-reference the context array for an explicit country key and apply a stricter threshold (≥ 0.90) for addresses in regions with known OSM coverage gaps before routing ambiguous records to a fallback chain for failed lookups.
Integration Note
This comparison feeds directly into the comparing geocoding accuracy across providers workflow. In practice, the two providers are not mutually exclusive: route HERE for nightly bulk normalization of freight manifests and use Mapbox for real-time driver-app queries where OSM road network freshness matters more than postal precision. Cache successful normalizations by the SHA-256 cache_key from the model above — logistics manifests frequently contain the same address with minor formatting variations, and caching normalized payloads at this key reduces API spend substantially. For pipelines that must track per-provider costs across both providers, see tracking API spend with Python and Redis.
Related
- Comparing Geocoding Accuracy Across Providers — methodology for setting up precision baselines and confidence cut-offs across any set of geocoding APIs.
- Implementing Fallback Chains for Failed Lookups — how to chain HERE → Mapbox → OpenStreetMap with circuit-breaker logic and dead-letter queues.
- API Quota Tracking and Cost Management — monitor per-provider spend and enforce budget caps before an overnight batch run exhausts your HERE or Mapbox quota.
- Building Async Geocoding Requests in Python — asyncio-based request patterns for pushing HERE and Mapbox endpoints closer to their rate-limit ceilings.