Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 36 additions & 19 deletions src/geocodio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,26 +379,43 @@ def _parse_geocoding_response(self, response_json: dict) -> GeocodingResponse:
and response_json["results"]
and "response" in response_json["results"][0]
):
results = [
GeocodingResult(
address_components=AddressComponents.from_api(
res["response"]["results"][0]["address_components"]
),
formatted_address=res["response"]["results"][0][
"formatted_address"
],
location=Location(**res["response"]["results"][0]["location"]),
accuracy=res["response"]["results"][0].get("accuracy", 0.0),
accuracy_type=res["response"]["results"][0].get(
"accuracy_type", ""
),
source=res["response"]["results"][0].get("source", ""),
fields=self._parse_fields(
res["response"]["results"][0].get("fields")
),
results = []
for res in response_json["results"]:
query = res.get("query", "")
matches = res.get("response", {}).get("results") or []

# Unmatched query (e.g. an unparseable address): keep an entry
# so the result list stays aligned with the submitted addresses,
# but with no coordinates.
if not matches:
results.append(
GeocodingResult(
address_components=AddressComponents.from_api({}),
formatted_address="",
location=None,
accuracy=0.0,
accuracy_type="",
source="",
query=query,
)
)
continue

top = matches[0]
results.append(
GeocodingResult(
address_components=AddressComponents.from_api(
top["address_components"]
),
formatted_address=top["formatted_address"],
location=Location(**top["location"]),
accuracy=top.get("accuracy", 0.0),
accuracy_type=top.get("accuracy_type", ""),
source=top.get("source", ""),
query=query,
fields=self._parse_fields(top.get("fields")),
)
)
for res in response_json["results"]
]
return GeocodingResponse(results=results)

# Handle single response format
Expand Down
8 changes: 7 additions & 1 deletion src/geocodio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,11 +665,17 @@ def from_api(cls, data: Dict[str, Any]) -> "DistanceJobResponse":
class GeocodingResult:
address_components: AddressComponents
formatted_address: str
location: Location
location: Optional[Location]
accuracy: float
accuracy_type: str
source: str
fields: Optional[GeocodioFields] = None
query: str = ""

@property
def matched(self) -> bool:
"""True when the API returned coordinates for this query."""
return self.location is not None


@dataclass(slots=True, frozen=True)
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/test_geocode.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,3 +748,75 @@ def response_callback(request):

# Ensure state_legislative_districts didn't leak into extras
assert "state_legislative_districts" not in fields.extras


def test_geocode_batch_with_unmatched_address(client, httpx_mock):
"""Batch responses can include queries with no results (e.g. an address
the API could not match). Those entries must not raise IndexError, and the
result list stays aligned with the submitted addresses so callers can tell
which query failed."""
addresses = ["3730 N Clark St, Chicago, IL", "qwertyuiop asdfghjkl zxcvbnm"]

def batch_response_callback(request):
return httpx.Response(
200,
json={
"results": [
{
"query": "3730 N Clark St, Chicago, IL",
"response": {
"results": [
{
"address_components": {
"number": "3730",
"predirectional": "N",
"street": "Clark",
"suffix": "St",
"city": "Chicago",
"county": "Cook County",
"state_province": "IL",
"postal_code": "60613",
"country": "US",
},
"formatted_address": "3730 N Clark St, Chicago, IL 60613",
"location": {"lat": 41.94987, "lng": -87.65893},
"accuracy": 1,
"accuracy_type": "rooftop",
"source": "Cook",
}
]
},
},
{
"query": "qwertyuiop asdfghjkl zxcvbnm",
"response": {
"error": "Could not geocode address. No matches found.",
"results": [],
},
},
]
},
)

httpx_mock.add_callback(
callback=batch_response_callback,
url=httpx.URL("https://api.test/v2/geocode"),
match_headers={"Authorization": "Bearer TEST_KEY"},
)

resp = client.geocode(addresses)

# One entry per submitted address, in order.
assert len(resp.results) == 2

matched, unmatched = resp.results
assert matched.matched is True
assert matched.query == "3730 N Clark St, Chicago, IL"
assert matched.formatted_address == "3730 N Clark St, Chicago, IL 60613"
assert matched.location.lat == 41.94987

# Unmatched query is preserved with no coordinates.
assert unmatched.matched is False
assert unmatched.location is None
assert unmatched.query == "qwertyuiop asdfghjkl zxcvbnm"
assert unmatched.formatted_address == ""
Loading