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.
From Score to Action
A score that does not change behaviour is decoration. Three bands cover the useful responses, and their boundaries should be set from the cost of each error rather than from round numbers — the review band exists precisely because some records are worth a human minute and most are not.
Size the review band against the review capacity you actually have. A band that produces six percent of a million-row batch is sixty thousand records, which is not a queue anyone will clear — either the band narrows, or the review is sampled and the rest are accepted with the band recorded.
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.
Keeping the Score Explainable
Storing only the final number makes every later question unanswerable. Persist the inputs alongside it, and both debugging and re-tuning become offline exercises over existing rows rather than a new production experiment.
The weights_version column prevents a subtle reporting error. When the weights change,
scores computed before and after are not comparable, and a dashboard that averages across
the change shows a step that looks like a quality event and is an accounting artefact.
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.
Where Composite Scoring Misleads
A composite is an average, and averages hide bimodality. Two very different records can score identically — one mediocre on everything, one excellent on three inputs and failing the fourth — and the second is frequently the dangerous one. Guard the components that must not fail with a floor rather than a weight.
Implement the floor as a separate check rather than by inflating the weight. Raising the geometric weight enough to sink record B also sinks records where the polygon data is merely incomplete, and you end up trading a rare dangerous error for a common annoying one. A floor addresses only the case it is meant to.
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.