Skip to content

feat: canonicalize addresses#108

Open
scorinaldi wants to merge 1 commit into
mainfrom
feature/canonical-address-components
Open

feat: canonicalize addresses#108
scorinaldi wants to merge 1 commit into
mainfrom
feature/canonical-address-components

Conversation

@scorinaldi

@scorinaldi scorinaldi commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Description

My first crack at this, heavily supported by Claude. Please have a look and let me know what you think. Thanks!

Related Issue

#16

How Has This Been Tested?

Changes made to automatic tests

Summary by CodeRabbit

  • New Features

    • Enhanced address data enrichment with detailed components including country, region, locality, and postal code information from external data sources.
    • Improved address information preservation and normalization across data pipeline updates.
  • Tests

    • Added comprehensive test coverage for address component extraction and enrichment logic.
  • Chores

    • Updated development dependencies.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR extends the enrichment pipeline to extract and preserve structured address components from Google Places API responses, adding new model fields, parsing logic, country inference from enrichment cache, and comprehensive test coverage for ambiguous guide location inference.

Changes

Address Components Enrichment

Layer / File(s) Summary
EnrichmentPlace data model schema
scripts/pipeline_models.py
EnrichmentPlace adds optional fields for formatted addresses (formatted_address_en, formatted_address_local) and structured address components (address_country_name, address_country_code, address_admin_area, address_locality, address_postal_code).
Google Places API address extraction and normalization
scripts/build_data.py
API request field mask now includes addressComponents. New extract_api_address_components helper parses component types and extracts country, admin area, locality, and postal code. API enrichment match normalization sets formatted_address_en from API formattedAddress and populates all address component fields in EnrichmentPlace.
Country inference and guide normalization
scripts/build_data.py
New infer_country_from_enrichment_cache helper votes across publishable enrichment cache entries to determine the most common country pair. normalize_guide now falls back to cache-inferred country when title-based inference cannot determine country_name and country_code.
Enrichment field canonicalization and preservation
scripts/build_data.py
canonicalize_enrichment_place sanitizes formatted_address_en. preserve_existing_enrichment carries forward formatted_address_en and address component fields when missing in refreshed enrichment results.
Address extraction and inference test coverage
tests/python/test_build_data.py
Tests validate extract_api_address_components for Monaco and Okinawa address parsing, normalize_enrichment_match population of formatted address and country fields, infer_country_from_enrichment_cache voting and None behavior, and normalize_guide fallback to cache-inferred country for ambiguous place names.
Development dependency update
pyproject.toml
pytest version constraint updated to >=9.0.3 in the dev dependency group.

Sequence Diagram

sequenceDiagram
  participant PlacesAPI as Google Places API
  participant extract as extract_api_address_components
  participant normalize as normalize_enrichment_match
  participant cache as enrichment_cache
  participant infer as infer_country_from_enrichment_cache
  participant guide as normalize_guide
  participant model as EnrichmentPlace
  PlacesAPI->>extract: addressComponents list
  extract->>normalize: {country_name, country_code, admin_area, locality, postal_code}
  normalize->>model: formatted_address_en, address_* fields
  cache->>infer: scan publishable entries
  infer->>guide: most common (country_name, country_code)
  guide->>model: fallback country when title inference fails
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Addresses now bloom with structure divine,
Components extracted in perfect design,
From Monaco to Okinawa shores,
Country inference opens new doors,
The enrichment cache now helps guides align!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: canonicalize addresses' directly aligns with the main changes: adding address canonicalization logic, extending the EnrichmentPlace model with address components, and implementing address normalization across multiple scripts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/canonical-address-components

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread pyproject.toml
[dependency-groups]
dev = [
"mypy>=1.18.2,<2.0.0",
"pytest>=9.0.3",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't able to find pytest when I used uv so I added this, apologies if not needed!

Comment thread uv.lock
@@ -1,14 +1,14 @@
version = 1
revision = 1
revision = 3

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know if I should remove this from the PR , thanks!

@scorinaldi scorinaldi requested a review from michaelmwu May 22, 2026 00:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pyproject.toml (1)

19-19: ⚡ Quick win

Constrain pytest to the intended major range.

pytest>=9.0.3 is unbounded. The latest stable release is pytest 9.0.3, and the pytest project reserves breaking-deprecation removals for major releases—pytest 10 is already documented with breaking removals/deprecations. To prevent surprise CI failures, match the neighboring dev-dependency style by using an upper bound like pytest>=9.0.3,<10.0.0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 19, The pytest dev-dependency is unbounded
("pytest>=9.0.3"), which can allow pytest 10 with breaking changes; update the
dependency string to constrain the major range (for example change
"pytest>=9.0.3" to "pytest>=9.0.3,<10.0.0") so CI isn't surprised by pytest 10;
locate the dependency entry containing pytest>=9.0.3 in pyproject.toml and add
the <10.0.0 upper bound.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build_data.py`:
- Around line 6461-6466: The current block only copies previous_place.address_*
fields when address_country_name is missing, which loses valid refreshed
subcomponents; update the logic to preserve each address component independently
by checking each field separately (for example, for address_country_name,
address_country_code, address_admin_area, address_locality, address_postal_code)
and assign refreshed_place.<field> = previous_place.<field> only when
refreshed_place.<field> is empty/falsey and previous_place.<field> is present,
using individual if checks for refreshed_place and previous_place on each named
field.

---

Nitpick comments:
In `@pyproject.toml`:
- Line 19: The pytest dev-dependency is unbounded ("pytest>=9.0.3"), which can
allow pytest 10 with breaking changes; update the dependency string to constrain
the major range (for example change "pytest>=9.0.3" to "pytest>=9.0.3,<10.0.0")
so CI isn't surprised by pytest 10; locate the dependency entry containing
pytest>=9.0.3 in pyproject.toml and add the <10.0.0 upper bound.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cda3bb0f-2f01-42fb-9447-a44e5b71f691

📥 Commits

Reviewing files that changed from the base of the PR and between 12c98ed and 9442195.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • pyproject.toml
  • scripts/build_data.py
  • scripts/pipeline_models.py
  • tests/python/test_build_data.py

Comment thread scripts/build_data.py
Comment on lines +6461 to +6466
if not refreshed_place.address_country_name and previous_place.address_country_name:
refreshed_place.address_country_name = previous_place.address_country_name
refreshed_place.address_country_code = previous_place.address_country_code
refreshed_place.address_admin_area = previous_place.address_admin_area
refreshed_place.address_locality = previous_place.address_locality
refreshed_place.address_postal_code = previous_place.address_postal_code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve address component fields independently.

On Line 6461, preservation of address_country_code, address_admin_area, address_locality, and address_postal_code is incorrectly gated by address_country_name. If refreshed data keeps country name but drops other components, prior valid data is lost.

💡 Proposed fix
-    if not refreshed_place.address_country_name and previous_place.address_country_name:
-        refreshed_place.address_country_name = previous_place.address_country_name
-        refreshed_place.address_country_code = previous_place.address_country_code
-        refreshed_place.address_admin_area = previous_place.address_admin_area
-        refreshed_place.address_locality = previous_place.address_locality
-        refreshed_place.address_postal_code = previous_place.address_postal_code
+    if not refreshed_place.address_country_name and previous_place.address_country_name:
+        refreshed_place.address_country_name = previous_place.address_country_name
+    if not refreshed_place.address_country_code and previous_place.address_country_code:
+        refreshed_place.address_country_code = previous_place.address_country_code
+    if not refreshed_place.address_admin_area and previous_place.address_admin_area:
+        refreshed_place.address_admin_area = previous_place.address_admin_area
+    if not refreshed_place.address_locality and previous_place.address_locality:
+        refreshed_place.address_locality = previous_place.address_locality
+    if not refreshed_place.address_postal_code and previous_place.address_postal_code:
+        refreshed_place.address_postal_code = previous_place.address_postal_code
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not refreshed_place.address_country_name and previous_place.address_country_name:
refreshed_place.address_country_name = previous_place.address_country_name
refreshed_place.address_country_code = previous_place.address_country_code
refreshed_place.address_admin_area = previous_place.address_admin_area
refreshed_place.address_locality = previous_place.address_locality
refreshed_place.address_postal_code = previous_place.address_postal_code
if not refreshed_place.address_country_name and previous_place.address_country_name:
refreshed_place.address_country_name = previous_place.address_country_name
if not refreshed_place.address_country_code and previous_place.address_country_code:
refreshed_place.address_country_code = previous_place.address_country_code
if not refreshed_place.address_admin_area and previous_place.address_admin_area:
refreshed_place.address_admin_area = previous_place.address_admin_area
if not refreshed_place.address_locality and previous_place.address_locality:
refreshed_place.address_locality = previous_place.address_locality
if not refreshed_place.address_postal_code and previous_place.address_postal_code:
refreshed_place.address_postal_code = previous_place.address_postal_code
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build_data.py` around lines 6461 - 6466, The current block only
copies previous_place.address_* fields when address_country_name is missing,
which loses valid refreshed subcomponents; update the logic to preserve each
address component independently by checking each field separately (for example,
for address_country_name, address_country_code, address_admin_area,
address_locality, address_postal_code) and assign refreshed_place.<field> =
previous_place.<field> only when refreshed_place.<field> is empty/falsey and
previous_place.<field> is present, using individual if checks for
refreshed_place and previous_place on each named field.

@michaelmwu michaelmwu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Codex generated review. Please validate.

Thanks for taking a crack at this. I think the contribution is directionally useful, but I would not merge it as-is.

The useful part is the Google Places addressComponents extraction. That is still relevant even after the translation-memory work: translation memory helps produce English-readable display addresses from string fields, but #16 is about machine-trustworthy canonical country/address metadata from the API. Structured API components are a better source for country/admin/locality/postal-code inference than translated address strings.

The parts I would change before merging:

  • formatted_address_en mostly duplicates the current formatted_address / address_display_en path on main, where API lookups already request English and display normalization already prefers address_display_en where available.
  • formatted_address_local is added but not populated, so this does not yet satisfy the local-language preservation goal from #16.
  • The new places.addressComponents field mask needs cache invalidation/versioning. Otherwise old enrichment cache entries can remain valid but never gain the newly requested fields. This probably belongs in the enrichment input signature/policy payload.
  • The preservation logic should copy each address component independently. A refreshed result could have a new country but be missing locality/postal code, and the current all-or-nothing block would drop useful previous subfields.
  • The pytest>=9.0.3 and large uv.lock rewrite look unrelated to this feature and should be removed or minimized.

Suggested path: keep the core idea, but narrow the patch to API address component extraction + model fields + country inference fallback + focused tests + explicit enrichment cache invalidation. I would skip formatted_address_en/local for now unless this PR also implements true dual-language API lookups.

@scorinaldi

Copy link
Copy Markdown
Collaborator Author

Thanks @michaelmwu -- will make these changes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants