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.

Match Types Do Not Line Up

Both providers return a quality signal and they are not the same signal. Google reports a location_type describing how the coordinate was derived; HERE reports a resultType and a per-field match summary describing which components matched. Reading one as though it were the other is the most common source of wrong conclusions in a head-to-head test.

Aligning Google location_type with HERE resultType A four-row alignment table. Google ROOFTOP aligns with HERE houseNumber at the rooftop tier. Google RANGE_INTERPOLATED aligns with HERE houseNumber interpolated at the interpolated tier. Google GEOMETRIC_CENTER aligns with HERE street at the street tier. Google APPROXIMATE aligns with HERE locality at the locality tier. Google location_type HERE equivalent Shared tier ROOFTOP houseNumber (exact) rooftop RANGE_INTERPOLATED houseNumber (interpolated) interpolated GEOMETRIC_CENTER street street APPROXIMATE locality · postalCode locality

The second row is where the two diverge most in practice. Interpolated results are placed along a street segment by proportion rather than observed, and both providers issue them freely in newer suburban developments. Counting them as rooftop inflates the quality figure for whichever provider issues more of them, which is why the shared tier keeps them separate.

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.

Licensing Shapes the Architecture

Accuracy is only half the comparison. The two providers differ substantially in what you are permitted to store, and that constraint propagates into the cache design, the database schema and the retention policy — decisions that are expensive to reverse once a few hundred million rows exist.

What each licence lets you keep, and for how long Two panels. The Google panel notes that caching is restricted and generally time-limited, so the architecture must treat the cache as a short-lived accelerator rather than a store of record. The HERE panel notes that storage rights are available under specific plan tiers, permitting a durable coordinate column when the contract allows it. Both panels stress reading the current contract rather than relying on summaries. Restricted storage cache is an accelerator, not a system of record TTL bounded by the contract, not by hit rate warehouse keeps the address, not the coordinate re-resolve on demand; budget for the repeat calls design consequence: higher steady-state spend Storage-permitted tiers durable coordinate column is contractually allowed cache TTL chosen for freshness, not compliance spatial indexes can be built once and kept refresh becomes a scheduling question design consequence: fixed cost, durable schema Confirm the terms in your own signed agreement before designing around either column — plan tiers and terms change.

If your workload re-reads the same addresses often — a delivery network, a subscription service — the storage question can outweigh a couple of percentage points of match rate. A durable coordinate column removes the repeat calls entirely, which is a structural cost change rather than an incremental one.

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.

Deciding From Your Own Numbers

The output of a head-to-head test should be a routing rule, not a winner. Both providers will lead somewhere in your mix, and the useful artefact is the decision table that turns the measurements into behaviour — including the case where the difference is inside the noise and the cheaper provider should therefore win.

From benchmark numbers to a routing rule Three outcome bands. When one provider leads rooftop share by more than three points in a market, it becomes primary there. When the gap is within noise, the cheaper provider becomes primary and the other becomes fallback. When neither clears the accuracy bar for that market, the records are routed to review rather than trusted. gap > 3 points rooftop share the leader becomes primary in that market; the other stays in the chain as fallback gap within the confidence interval price decides — a two-tenths-of-a-point lead is not worth a premium per call neither clears the bar for this market route to review or a local data source; do not automate decisions on either coordinate

Include the confidence interval in the second band deliberately. On a sample of a few thousand records, a one-point difference in rooftop share is frequently not distinguishable from sampling noise, and rebuilding a routing table around it produces churn without benefit. Compute the interval, and treat overlapping intervals as a tie decided on cost.

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.