Choosing Between HERE and Mapbox for Logistics Geocoding

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.

HERE vs Mapbox logistics geocoding pipeline A flowchart showing raw address input flowing through pre-flight cleaning, then splitting to HERE (primary, batch) and Mapbox (fallback, real-time), with confidence thresholds gating dispatch or manual review. Raw address input Pre-flight cleaning HERE (primary/batch) Mapbox (fallback/RT) resultType=houseNumber queryScore ≥ 0.8? place_type=address relevance ≥ 0.85? yes yes Dispatch store no no Manual review / fallback queue

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.