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.
Related
- Comparing Geocoding Accuracy Across Providers — the benchmarking methodology for setting confidence thresholds against your own labelled corpus.
- Dynamic Provider Selection Based on Region — route Google or HERE first depending on the country where each has the deepest coverage.
- Implementing Fallback Chains for Failed Lookups — chain the two providers with circuit breakers so a partial match escalates automatically.
- Validating Geocoding Accuracy and Confidence Scoring — turn
location_typeandqueryScoreinto calibrated, comparable confidence values.