TL;DR: A robust geocoding confidence score is a weighted composite of three signals — match type (50%), component-match completeness (30%), and the provider’s own normalized score (20%) — blended into a single 0-100 value you can threshold. This page gives a typed Python function and a pandas column; it expands the scoring step of validating geocoding accuracy and confidence scoring.
Why a Composite Score Beats a Single Field
A provider’s raw confidence answers “how sure am I this is the right record?” but ignores two things you care about: how precise the returned geometry is, and whether the result actually contains the components you searched for. A relevance of 0.95 on a result that echoes back only a city — dropping the street and house number you supplied — is a confident city centroid, not a confident rooftop. A composite score fixes this by combining a precision signal, a completeness signal, and the provider signal so that no single flattering field can carry a bad result over the line.
The three components are deliberately orthogonal:
- Match type captures geometry precision (rooftop vs interpolated vs centroid).
- Component completeness captures how much of the requested address the result reproduces.
- Provider score captures the vendor’s own record-match confidence.
Score Weight Breakdown
Each component is computed on a 0-1 scale, multiplied by its weight, summed, and scaled to 0-100. The weights below are tuned for delivery and rooftop-dependent workloads; lower the match-type weight for analytics that tolerate centroids.
| Component | Weight | Signal it captures | 1.0 means | 0.0 means |
|---|---|---|---|---|
| Match type | 0.50 | Geometry precision | Rooftop / exact house match | Region or country centroid |
| Component completeness | 0.30 | Requested fields returned | House, street, city, postcode all echoed | Only a locality returned |
| Provider score | 0.20 | Vendor record-match confidence | Provider score 1.0 (rescaled) | Provider score 0.0 |
| Composite | 1.00 | Overall trust | 100 | 0 |
A composite of 85+ is a safe automatic accept; 55–85 warrants a fallback or review; below 55 should be rejected. These cut-offs mirror the accept/fallback/reject tiers in the parent guide.
The Scoring Function
The function scores each component independently, then blends. match_type is looked up against a precision table, completeness is the fraction of requested components present in the response, and the provider score is clamped to 0-1.
from __future__ import annotations
from dataclasses import dataclass
from typing import Mapping, Optional, Sequence
# Match-type precision on a 0-1 scale (provider-neutral vocabulary).
_MATCH_TYPE_SCORE: dict[str, float] = {
"rooftop": 1.0,
"exact": 1.0,
"range_interpolated": 0.8,
"street": 0.6,
"geometric_center": 0.4,
"postcode": 0.35,
"locality": 0.2,
"approximate": 0.15,
"region": 0.1,
}
_W_MATCH = 0.50
_W_COMPLETENESS = 0.30
_W_PROVIDER = 0.20
@dataclass
class ScoredResult:
composite: int # 0-100
match_component: float # 0-1
completeness: float # 0-1
provider_component: float # 0-1
def _completeness(
requested: Sequence[str],
returned: Mapping[str, Optional[str]],
) -> float:
"""Fraction of requested components the result actually echoes back."""
if not requested:
return 1.0
present = sum(
1 for c in requested
if returned.get(c) not in (None, "", "null")
)
return present / len(requested)
def confidence_score(
match_type: str,
requested_components: Sequence[str],
returned_components: Mapping[str, Optional[str]],
provider_score: Optional[float],
) -> ScoredResult:
"""Compute a weighted composite geocoding confidence score (0-100).
Args:
match_type: Provider-neutral precision label (e.g. 'rooftop').
requested_components: Components supplied in the query, e.g.
['house', 'street', 'city', 'postcode'].
returned_components: Mapping of component name to returned value.
provider_score: Provider-native confidence in [0, 1], or None.
Returns:
A ScoredResult with the composite and its three sub-scores.
Raises:
ValueError: If provider_score is outside [0, 1].
"""
match_component = _MATCH_TYPE_SCORE.get(match_type.lower(), 0.0)
completeness = _completeness(requested_components, returned_components)
if provider_score is None:
# No native score: lean on match type as the provider proxy.
provider_component = match_component
else:
if not 0.0 <= provider_score <= 1.0:
raise ValueError(f"provider_score out of range: {provider_score}")
provider_component = float(provider_score)
composite01 = (
_W_MATCH * match_component
+ _W_COMPLETENESS * completeness
+ _W_PROVIDER * provider_component
)
return ScoredResult(
composite=round(composite01 * 100),
match_component=round(match_component, 3),
completeness=round(completeness, 3),
provider_component=round(provider_component, 3),
)
A worked example — a rooftop match that returns every requested component with a strong provider score:
scored = confidence_score(
match_type="rooftop",
requested_components=["house", "street", "city", "postcode"],
returned_components={
"house": "221B", "street": "Baker St",
"city": "London", "postcode": "NW1 6XE",
},
provider_score=0.93,
)
print(scored.composite) # 99
And a flattering-but-shallow result — high provider score but only a city echoed back:
scored = confidence_score(
match_type="locality",
requested_components=["house", "street", "city", "postcode"],
returned_components={"city": "London"},
provider_score=0.95,
)
print(scored.composite) # 36 -> correctly rejected
The 0.95 provider score cannot rescue a locality-level match that dropped three of four requested components. That is exactly the failure a single-field threshold misses.
Vectorized pandas Column
For batch scoring, compute each component as a vectorized column and blend. Completeness is derived from per-component presence flags you materialize once during parsing.
import numpy as np
import pandas as pd
_MATCH_TYPE_MAP = pd.Series(_MATCH_TYPE_SCORE)
def score_frame(
df: pd.DataFrame,
match_type_col: str = "match_type",
provider_col: str = "provider_score",
completeness_col: str = "completeness",
) -> pd.DataFrame:
"""Add a 0-100 'confidence' column to a DataFrame of geocoding results.
Expects a precomputed 'completeness' column in [0, 1] (fraction of
requested components returned) and a provider score in [0, 1].
"""
out = df.copy()
match = (
out[match_type_col].str.lower().map(_MATCH_TYPE_MAP).fillna(0.0)
)
provider = pd.to_numeric(out[provider_col], errors="coerce")
completeness = pd.to_numeric(out[completeness_col], errors="coerce").fillna(0.0)
# Where provider score is missing, use match type as the proxy.
provider = provider.fillna(match).clip(0.0, 1.0)
composite = (
_W_MATCH * match
+ _W_COMPLETENESS * completeness
+ _W_PROVIDER * provider
)
out["confidence"] = (composite * 100).round().astype("Int64")
out["verdict"] = np.select(
[out["confidence"] >= 85, out["confidence"] >= 55],
["accept", "fallback"],
default="reject",
)
return out
Materialize completeness upstream while you still have the raw response, since it needs both the requested and returned component sets — reconstructing it after flattening to columns is error-prone.
Edge Cases
Provider returns extra components you did not request
Completeness measures requested-vs-returned, so extra components are ignored by design. Do not credit a result for returning a state you never asked for — it inflates the score without improving the match.
Missing provider score for the whole batch
Google Maps exposes no numeric confidence, only location_type. The function substitutes the match-type component as the provider proxy so Google results are not unfairly zeroed. Keep the substitution explicit rather than defaulting provider_score to 0.0, which would cap every Google composite at 80.
Integration Note
This composite score is the numeric input to the accept/fallback/reject routing described in validating geocoding accuracy and confidence scoring. Pair it with a geometric sanity check: even a composite of 95 should be re-examined if the point sits far from its expected reference, which is where detecting geocoding outliers with haversine distance comes in. Together, a high composite plus a small reference distance is the strongest signal that a geocode is safe to commit.
Related
- Validating geocoding accuracy and confidence scoring — the three-tier validator that consumes this composite score for routing.
- Detecting geocoding outliers with haversine distance — the geometric check that complements a high composite score.
- Comparing geocoding accuracy across providers — how normalized scores let you rank providers on the same scale.