Skip to content

feat: reverse geolocation per polygon strategy#1318

Merged
davidgamez merged 36 commits into
mainfrom
feat/reverse_geolocation_per_polygon_strategy
Aug 27, 2025
Merged

feat: reverse geolocation per polygon strategy#1318
davidgamez merged 36 commits into
mainfrom
feat/reverse_geolocation_per_polygon_strategy

Conversation

@davidgamez

@davidgamez davidgamez commented Aug 13, 2025

Copy link
Copy Markdown
Member

Summary:

Closes #1301, #1289, #965
Significant changes in this PR:

  • Introduced per_polygon strategy
    • This strategy, described here, looks for clustering points within a polygon to reduce the amount of DB calls. This works for cases where the points are close together and the geolocation query has "high" admin levels in their respective country. In cases where the admin levels are below the municipality, the algorithm performs as a "per_point" strategy.
  • Fixed duplicate key value violates unique constraint. This was happening because the per-point strategy was attempting to create the same location multiple times within the same database session.
  • Added a guard against multiple executions of the function within the same day.
  • Stop using stop caching for GBFS, as vehicle positions change constantly.
  • Fixed duplicate admin levels. This issue had two causes(Investigate location extraction failures caused by duplicate admin levels #965):
    • The difference between the two representations (plane 2D and geospatial 3D) was causing duplicate intersections between polygons on large areas and edges. To fix this, the code now is casting the geometry as Geography.
    • OSM doesn’t enforce uniqueness at each level; different contributors can model administrative boundaries differently. To fix this, a tie-break mechanism was implemented.

Minor fixes

  • Fixed refresh materialized view function name in cloud task
  • Missing GCP function region and project ID parameters

Out of scope:

Expected behavior:

  • ✅ A reduced running time for feeds with a high number of stops per polygon.
  • ✅ Reverse geolocation function runs a maximum of once per feed day and feed ID.

Performance results

The performance results were collected for a few feeds without using the geometry cache.

Feed Stops Locations Strategy Time Memory(peak) Notes
GTFS --- --- --- --- --- ---
mdb-2014 308611 225 Per-Point 33130s(9.2h) 129.57 MB
Per-Polygon 25870s(7.2h) 133.11 MB
--- --- --- --- --- --- ---
mdb-1139 663 324 Per-Point 327s(5.45m) 19.48 MB
Per-Polygon 100(1.6m) 129.57 MB
--- --- --- --- --- --- ---
mdb-2825 10985 1541 Per-Point 1052.04(17.5) 25.55 MB
Per-Polygon 348.9s(5.8m) 34.48 MB
GBFS --- --- --- --- --- ---
gbfs-flamingo_auckland 1539 1 Per-Point 37.2s 1.34 MB
Per-Polygon 37.9 1.24 MB All points reverse geolocation queries returns adminlevel 4
gbfs-hellocycling 11777 241 Per-Point 337.76s(5.6m) 14.16 MB
Per-Polygon 31.59 9.81 MB

Testing tips:
Local testing

  • Use reverse_geolocation_process_verifier.py for local testing

From our AI friend:

This pull request introduces new geospatial utilities and refactors location handling logic to improve reverse geolocation and location grouping features. It adds helper functions for working with geopolygons, enhances enum conversion error reporting, and expands test coverage for location utilities. Several dependencies and configuration files are updated to support these changes.

Geospatial utilities and location logic enhancements

  • Added new helper functions in locations.py for converting geometry types (to_shapely), selecting geopolygons by admin level (select_highest_level_polygon, select_lowest_level_polygon), extracting country codes from polygons, and querying geopolygons that cover a point with BigQuery-compatible semantics.
  • Introduced new reverse geolocation utilities in location_group_utils.py, including functions for extracting location aggregates, creating/updating stop groups, and managing location group entities. These functions leverage the new geopolygon helpers for more robust location assignment and grouping. [1] [2]

Test coverage improvements

  • Expanded test_locations.py with comprehensive tests for the new geospatial helper functions, including edge cases for admin level selection and country code extraction from geopolygons.

Dependency and configuration updates

  • Added shapely to requirements.txt to support geometry conversions and spatial operations.
  • Updated .coveragerc to omit coverage for files in */shared/common/*, reflecting the new shared helpers.

Error handling and logging improvements

  • Improved error reporting in enum conversion by logging failed conversions in transform.py.
  • Updated logger initialization to import google.cloud.logging only when needed, improving startup reliability.

Minor fixes

  • Fixed a typo in the Cloud Function URL construction in gcp_utils.py (tasks_executor instead of tasks-executor).
  • Added missing import for logging in transform.py.
  • Cleaned up unused imports in logger.py.
  • Added new enum values and documentation for reverse geocoding strategies in locations.py.

Please make sure these boxes are checked before submitting your pull request - thanks!

  • Run the unit tests with ./scripts/api-tests.sh to make sure you didn't break anything
  • Add or update any needed documentation to the repo
  • Format the title like "feat: [new feature short description]". Title must follow the Conventional Commit Specification(https://www.conventionalcommits.org/en/v1.0.0/).
  • Linked all relevant issues
  • Include screenshot(s) showing how this pull request works and fixes the issue(s)

Comment thread functions-python/reverse_geolocation/src/strategy_extraction_per_polygon.py Outdated
import logging
import threading

import google.cloud.logging

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Moving this import inside the function, as the unit tests can fail when running locally, and the console is not authenticated with Google services

@davidgamez davidgamez marked this pull request as ready for review August 25, 2025 20:41
@davidgamez davidgamez requested a review from cka-y August 25, 2025 20:42
@davidgamez davidgamez requested a review from qcdyx August 25, 2025 22:02

@qcdyx qcdyx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Excellent work!

@cka-y cka-y left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 🏆


def get_geopolygons_covers(stop_point: WKTElement, db_session: Session):
"""
Get all geopolygons that cover a given point using BigQuery-compatible semantics.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[question] what does this have to do w big query?

@davidgamez davidgamez Aug 27, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The fact that we have to cast(Geopolygon.geometry, Geography(srid=4326)) due to the original values from BigQuery tables being Geography instead of geometry

return abs(area)


def resolve_same_level_conflicts(items: List[Geopolygon]) -> Geopolygon:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

great addition 🤩

@davidgamez davidgamez merged commit 6f9d0e9 into main Aug 27, 2025
2 of 3 checks passed
@davidgamez davidgamez deleted the feat/reverse_geolocation_per_polygon_strategy branch August 27, 2025 19:49
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.

Optimize GBFS geolocation extraction: Use OSM polygons as query cache

4 participants