Google Maps vs HERE Geocoding Accuracy: A Practitioner Comparison

Choose Google Maps Platform Geocoding when you need the deepest rooftop-level coverage in North America and are willing to keep results ephemeral, and choose HERE Geocoding & Search when you need native batch throughput, permissive result-storage rights, and stronger structured match semantics for global postal data — see the full comparing geocoding accuracy across providers guide for how to benchmark either choice against your own corpus.

Decision Matrix

The two providers differ less on raw coordinate accuracy in dense metros than on the surrounding contract: how they describe a match, whether you can persist the answer, and how a bulk manifest actually gets processed. Those three axes usually decide the primary slot in a multi-API routing and fallback chain.

Axis Google Maps Platform Geocoding HERE Geocoding & Search
Match granularity location_type: ROOFTOP, RANGE_INTERPOLATED, GEOMETRIC_CENTER, APPROXIMATE resultType + scoring.queryScore (0–1) + field scores
Partial-match flag partial_match: true boolean Per-field scoring.fieldScore degradation
Rooftop coverage Strongest in US/CA and major global metros Strong in EU; strong global postal normalization
Native batch No batch endpoint; per-request only Async Batch Geocoder (up to 10k addresses/job)
Pricing model Per-1,000 requests, tiered monthly credit Per-1,000 requests, transaction bundles
Result storage Restricted — caching mostly prohibited by ToS Permitted for 30 days (verify current terms)
Coordinate shape geometry.location.lat / .lng position.lat / position.lng

Match-Type Semantics

The single most important difference for pipeline logic is how each provider communicates confidence. Google’s location_type is a coarse enum. ROOFTOP means the point corresponds to a precise address; RANGE_INTERPOLATED means it was estimated between two known points along a road; GEOMETRIC_CENTER returns the centroid of a polyline or polygon; and APPROXIMATE is a fallback to a locality or postal centroid. Google pairs this with a partial_match boolean that flags when the geocoder had to relax the query to return anything at all — a critical signal that the input did not fully match.

HERE takes a numeric-and-structured approach. Its resultType (houseNumber, street, postalCode, locality, administrativeArea) plays the role of Google’s location_type, but HERE layers a scoring.queryScore float and per-component scoring.fieldScore values on top. That lets you detect that a house number matched perfectly while the street name was fuzzy — a distinction Google collapses into a single boolean. For automated address verification where you must decide per-record whether to trust the coordinate, HERE’s field-level scores map more directly onto rules. When you set thresholds, validate them empirically rather than by intuition, following the confidence-scoring approach in validating geocoding accuracy and confidence scoring.

The Result-Storage Constraint

This is the constraint that most often forces the decision. The Google Maps Platform terms restrict caching and storage of geocoding results, with a narrow exception permitting temporary caching of place IDs and limited retention to improve performance. In practice this means you generally cannot build a permanent lat/lon cache from Google results without violating the agreement. HERE’s terms historically permit storing geocoded coordinates for a defined window. If your architecture depends on a durable coordinate cache — which most high-volume batch pipelines do to control cost — HERE removes a legal blocker that Google imposes. Always re-read the current terms for both providers before designing persistence; these clauses change.

Unified Python Client

The client below calls either provider and normalizes both responses to one schema. It maps Google’s location_type and HERE’s resultType onto a shared match_level, and derives a common confidence float so downstream rules never branch on the raw provider.

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Optional

import requests

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

# Map Google location_type onto a normalized 0-1 confidence proxy.
_GOOGLE_LOC_CONFIDENCE: dict[str, float] = {
    "ROOFTOP": 1.0,
    "RANGE_INTERPOLATED": 0.75,
    "GEOMETRIC_CENTER": 0.5,
    "APPROXIMATE": 0.25,
}


@dataclass
class NormalizedGeocode:
    """Common schema for Google and HERE geocoding responses."""

    lat: float
    lon: float
    match_level: str          # ROOFTOP / houseNumber / street / ...
    confidence: float         # 0-1, comparable across providers
    partial_match: bool
    provider: str
    formatted: Optional[str] = None
    postal_code: Optional[str] = None
    raw_input: str = ""
    field_scores: dict = field(default_factory=dict)

    @property
    def is_precise(self) -> bool:
        """True when the result is safe for automated downstream use."""
        return self.confidence >= 0.75 and not self.partial_match


def parse_google(address: str, payload: dict) -> NormalizedGeocode:
    """Parse a Google Geocoding API JSON response."""
    status = payload.get("status", "UNKNOWN")
    results = payload.get("results", [])
    if status != "OK" or not results:
        raise ValueError(f"Google returned status={status!r} with no usable result")
    top = results[0]
    geom = top.get("geometry", {})
    loc = geom.get("location", {})
    loc_type = geom.get("location_type", "APPROXIMATE")
    postal = next(
        (c["long_name"] for c in top.get("address_components", [])
         if "postal_code" in c.get("types", [])),
        None,
    )
    return NormalizedGeocode(
        lat=float(loc.get("lat", 0.0)),
        lon=float(loc.get("lng", 0.0)),
        match_level=loc_type,
        confidence=_GOOGLE_LOC_CONFIDENCE.get(loc_type, 0.25),
        partial_match=bool(top.get("partial_match", False)),
        provider="google",
        formatted=top.get("formatted_address"),
        postal_code=postal,
        raw_input=address,
    )


def parse_here(address: str, payload: dict) -> NormalizedGeocode:
    """Parse a HERE Geocoding & Search v1 JSON response."""
    items = payload.get("items", [])
    if not items:
        raise ValueError("HERE returned no results")
    item = items[0]
    pos = item.get("position", {})
    scoring = item.get("scoring", {})
    addr = item.get("address", {})
    query_score = float(scoring.get("queryScore", 0.0))
    field_scores = scoring.get("fieldScore", {})
    # Treat any sub-1.0 field score as a partial match signal.
    partial = any(v < 1.0 for v in field_scores.values()) if field_scores else False
    return NormalizedGeocode(
        lat=float(pos.get("lat", 0.0)),
        lon=float(pos.get("lng", 0.0)),
        match_level=item.get("resultType", "unknown"),
        confidence=query_score,
        partial_match=partial,
        provider="here",
        formatted=addr.get("label"),
        postal_code=addr.get("postalCode"),
        raw_input=address,
        field_scores=field_scores,
    )


def geocode(address: str, provider: str, api_key: str) -> NormalizedGeocode:
    """Geocode one address via Google or HERE and normalize the response."""
    if provider == "google":
        url = "https://maps.googleapis.com/maps/api/geocode/json"
        params = {"address": address, "key": api_key}
        resp = requests.get(url, params=params, timeout=10)
        resp.raise_for_status()
        return parse_google(address, resp.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, timeout=10)
        resp.raise_for_status()
        return parse_here(address, resp.json())

    raise ValueError(f"Unsupported provider: {provider!r}")

Vectorized pandas batch

Because Google has no batch endpoint, both providers are driven row-wise here; for HERE at scale, prefer its async Batch Geocoder and reserve this loop for reconciliation or spot checks.

import pandas as pd


def geocode_frame(
    df: pd.DataFrame,
    address_col: str,
    provider: str,
    api_key: str,
) -> pd.DataFrame:
    """Geocode every row and append normalized columns.

    Failed rows are retained with NaN coordinates so the manifest
    stays aligned for a downstream fallback pass.
    """

    def _safe(addr: str) -> dict:
        try:
            return geocode(addr, provider, api_key).__dict__
        except (ValueError, requests.RequestException) as exc:
            logger.warning("geocode failed for %r: %s", addr, exc)
            return {"provider": provider, "raw_input": addr, "confidence": 0.0}

    expanded = df[address_col].map(_safe).apply(pd.Series)
    return df.join(expanded, rsuffix="_geo")

Edge Cases

Google partial_match with a valid-looking coordinate

A partial_match: true response can still carry a plausible ROOFTOP coordinate for a different address than requested — for example when a misspelled street resolves to a similarly named one nearby. Never treat location_type == "ROOFTOP" alone as authoritative; the is_precise property above gates on both the confidence proxy and the partial-match flag. Route anything that fails this gate into a fallback chain for failed lookups.

HERE queryScore versus fieldScore disagreement

HERE can return a high aggregate queryScore while an individual fieldScore.streets value sits at 0.8. For address verification you usually care about the weakest component, not the average. Inspect field_scores directly rather than trusting queryScore alone, and hold ambiguous records for review instead of auto-committing them.

Integration Note

These two providers pair naturally: because Google forbids durable result caching while HERE permits it within a window, a common pattern is to run HERE as the cacheable primary for bulk normalization and call Google only for the residual set where you need its North American rooftop depth, discarding Google coordinates after immediate use. Tie the threshold you use to gate that residual set to the methodology in the parent comparing geocoding accuracy across providers guide, and let a region-aware provider selector decide which engine leads for a given country.