GTFSの上下ポールをグループ化しバス停の重複表示を修正#1605
Conversation
parent_stationの階層を持たないフィード(西武・東急コミュニティバス・京王等) では上下ポールが別々のトップレベルstopとして取り込まれ、同名なのに別 station_g_cdになるため同じバス停名が2件表示されていた。同名かつ250m以内の stopをunion-findでまとめ、クラスタ代表(最小stop_id)のstation_g_cdを共有させる。 単独stopは従来値と一致するため都営など正しく動くフィードには影響しない。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughGTFS停留所の名称と距離を用いて近接ポールを統合し、代表stop_id由来の ChangesGTFS停留所グルーピング
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant GTFSStops
participant build_bus_station_g_cd_map
participant integrate_gtfs_stops_to_stations
participant Stations
GTFSStops->>build_bus_station_g_cd_map: 停留所名・緯度経度を渡す
build_bus_station_g_cd_map-->>integrate_gtfs_stops_to_stations: stop_idからstation_g_cdへのマップ
integrate_gtfs_stops_to_stations->>Stations: 統合代表に基づくstation_g_cdを割り当てる
integrate_gtfs_stops_to_stations->>Stations: 未登録stop_idは従来方式で割り当てる
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
…ping # Conflicts: # stationapi/src/import.rs
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@stationapi/src/import.rs`:
- Around line 2288-2300: Replace the pairwise union logic in the stop-grouping
loop with deterministic cluster assignment that only adds a stop when its
distance to every stop already in the candidate group is at most
BUS_STOP_GROUPING_RADIUS_METERS, preserving a maximum cluster diameter of 250m
rather than transitive connectivity. Update the surrounding grouping function to
use this assignment, and add a regression test covering three stops where A-B
and B-C are within the radius but A-C exceeds it, asserting A and C remain in
separate groups.
- Around line 2288-2302: Replace the all-pairs comparison loop over each
same-name stop bucket with a 250-meter spatial grid index, assigning stops to
grid cells and comparing each stop only with candidates in its own or adjacent
cells. Preserve the existing haversine-distance threshold and union-find updates
using find, parent, and BUS_STOP_GROUPING_RADIUS_METERS, while avoiding
duplicate candidate comparisons.
- Around line 3002-3004: Update the stop-loading SELECT and
build_bus_station_g_cd_map flow to retain whether each stop is referenced as a
parent_station. Use that metadata to keep parent stops mapped to their own
station_g_cd, while only grouping nearby or same-name poles for stops without a
parent hierarchy. Ensure independent hierarchy-less stops remain eligible for
the existing collapse behavior without merging with parent stops.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eac7f1bb-69f6-4e77-9cfa-bc8ca0a5ee14
📒 Files selected for processing (1)
stationapi/src/import.rs
| for a in 0..indices.len() { | ||
| for b in (a + 1)..indices.len() { | ||
| let sa = &stops[indices[a]]; | ||
| let sb = &stops[indices[b]]; | ||
| if haversine_distance(sa.stop_lat, sa.stop_lon, sb.stop_lat, sb.stop_lon) | ||
| <= BUS_STOP_GROUPING_RADIUS_METERS | ||
| { | ||
| let ra = find(&mut parent, a); | ||
| let rb = find(&mut parent, b); | ||
| if ra != rb { | ||
| parent[ra] = rb; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
連鎖した近接 stop により、250m を超える stop まで統合されます。
A-B と B-C が各250m以内なら、A-C が250m超でも union-find の推移閉包により同一グループになります。これは「遠距離の同名停留所は別グループ」という要件を破ります。クラスタ直径を250m以内に保つ決定的な割当てへ変更し、3点連鎖の回帰テストを追加してください。
🤖 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 `@stationapi/src/import.rs` around lines 2288 - 2300, Replace the pairwise
union logic in the stop-grouping loop with deterministic cluster assignment that
only adds a stop when its distance to every stop already in the candidate group
is at most BUS_STOP_GROUPING_RADIUS_METERS, preserving a maximum cluster
diameter of 250m rather than transitive connectivity. Update the surrounding
grouping function to use this assignment, and add a regression test covering
three stops where A-B and B-C are within the radius but A-C exceeds it,
asserting A and C remain in separate groups.
| for a in 0..indices.len() { | ||
| for b in (a + 1)..indices.len() { | ||
| let sa = &stops[indices[a]]; | ||
| let sb = &stops[indices[b]]; | ||
| if haversine_distance(sa.stop_lat, sa.stop_lon, sb.stop_lat, sb.stop_lon) | ||
| <= BUS_STOP_GROUPING_RADIUS_METERS | ||
| { | ||
| let ra = find(&mut parent, a); | ||
| let rb = find(&mut parent, b); | ||
| if ra != rb { | ||
| parent[ra] = rb; | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
同名 stop の全ペア走査を空間インデックスへ置き換えてください。
同名 bucket ごとに k*(k-1)/2 回の距離計算を行います。一般名の stop が多いフィードではインポートが二乗的に遅くなります。名称ごとの250mグリッドへ index 化し、同一・隣接セルだけを比較してください。
As per coding guidelines, “Avoid unnecessary SQL JOINs and redundant queries, and replace avoidable O(n×m) scans with O(n+m) indexed lookups such as HashMaps.”
🤖 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 `@stationapi/src/import.rs` around lines 2288 - 2302, Replace the all-pairs
comparison loop over each same-name stop bucket with a 250-meter spatial grid
index, assigning stops to grid cells and comparing each stop only with
candidates in its own or adjacent cells. Preserve the existing
haversine-distance threshold and union-find updates using find, parent, and
BUS_STOP_GROUPING_RADIUS_METERS, while avoiding duplicate candidate comparisons.
Source: Coding guidelines
| // Collapse the separate direction poles of one physical stop (feeds without a | ||
| // parent_station hierarchy expose each pole as its own top-level stop). | ||
| let station_g_cd_by_stop = build_bus_station_g_cd_map(&stops); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
親 stop と階層なし stop を区別してグループ化してください。
取得対象には parent_station の親として参照される stop も含まれますが、この map にはその識別情報がありません。そのため、近接・同名の親 stop や親 stop と独立 stop が統合され、parent_station 階層を持つフィードへ影響しないという要件を満たせません。既存SELECTで子を持つかを取得し、親 stop は従来どおり自身の station_g_cd に固定してください。
🤖 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 `@stationapi/src/import.rs` around lines 3002 - 3004, Update the stop-loading
SELECT and build_bus_station_g_cd_map flow to retain whether each stop is
referenced as a parent_station. Use that metadata to keep parent stops mapped to
their own station_g_cd, while only grouping nearby or same-name poles for stops
without a parent hierarchy. Ensure independent hierarchy-less stops remain
eligible for the existing collapse behavior without merging with parent stops.
概要
parent_station の階層を持たない GTFS フィードで、同じバス停の上下ポールが別々の駅グループとして登録され、アプリの検索結果に同名バス停が2件表示される不具合を修正する。
変更の種類
変更内容
station_g_cd単位でまとめて表示されるが、integrate_gtfs_stops_to_stationsはstation_g_cdをstop_idのハッシュから採番していた。parent_stationを持たないフィード(西武バス・東急コミュニティバス各種・京王バス等)は上下ポールがそれぞれ別のトップレベル stop として取り込まれるため、同名でも別station_g_cdになり、同じバス停名が2件出ていた。build_bus_station_g_cd_mapを追加し、同名かつ 250m 以内の stop を union-find でまとめ、クラスタ代表(最小stop_id)のstation_g_cdを共有させる。generate_bus_station_g_cdと同一値になるため、parent_stationを正しく持つ都営バスなど、既に正常なフィードには影響しない。test_build_bus_station_g_cd_map_merges_nearby_polesを追加(近接ポールの併合・遠隔同名の分離・単独stopの値不変を検証)。テスト
cargo fmt --all -- --checkが通ることcargo clippy -- -D warningsが通ることcargo test(SQLX_OFFLINE=true)が通ること補足: 本修正はインポート処理のため、反映にはバス停の再インポートが必要です。
関連Issue
スクリーンショット(任意)
Summary by CodeRabbit
新機能
バグ修正