Skip to content

GTFSの上下ポールをグループ化しバス停の重複表示を修正#1605

Open
TinyKitten wants to merge 2 commits into
devfrom
fix/bus-stop-pole-grouping
Open

GTFSの上下ポールをグループ化しバス停の重複表示を修正#1605
TinyKitten wants to merge 2 commits into
devfrom
fix/bus-stop-pole-grouping

Conversation

@TinyKitten

@TinyKitten TinyKitten commented Jul 12, 2026

Copy link
Copy Markdown
Member

概要

parent_station の階層を持たない GTFS フィードで、同じバス停の上下ポールが別々の駅グループとして登録され、アプリの検索結果に同名バス停が2件表示される不具合を修正する。

変更の種類

  • バグ修正
  • 新機能
  • データの修正・追加
  • リファクタリング
  • ドキュメント
  • CI/CD
  • その他

変更内容

  • バス停は station_g_cd 単位でまとめて表示されるが、integrate_gtfs_stops_to_stationsstation_g_cdstop_id のハッシュから採番していた。parent_station を持たないフィード(西武バス・東急コミュニティバス各種・京王バス等)は上下ポールがそれぞれ別のトップレベル stop として取り込まれるため、同名でも別 station_g_cd になり、同じバス停名が2件出ていた。
  • build_bus_station_g_cd_map を追加し、同名かつ 250m 以内の stop を union-find でまとめ、クラスタ代表(最小 stop_id)の station_g_cd を共有させる。
    • 近接スコープにより、遠く離れた同名別バス停(例: 西武と都営に約92km離れて存在する「新道」)は別グループのまま維持。
    • 単独 stop は従来の generate_bus_station_g_cd と同一値になるため、parent_station を正しく持つ都営バスなど、既に正常なフィードには影響しない。
    • 閾値はローカルの GTFS 実データで検証し、150〜350m のいずれでも >600m に広がるクラスタは発生しなかったため、安全側で 250m を採用。約5765 バス停 → 約3610 グループに集約(実データ上のフィード分)。
  • 単体テスト test_build_bus_station_g_cd_map_merges_nearby_poles を追加(近接ポールの併合・遠隔同名の分離・単独stopの値不変を検証)。

テスト

  • cargo fmt --all -- --check が通ること
  • cargo clippy -- -D warnings が通ること
  • cargo testSQLX_OFFLINE=true)が通ること

補足: 本修正はインポート処理のため、反映にはバス停の再インポートが必要です。

関連Issue

スクリーンショット(任意)

Summary by CodeRabbit

  • 新機能

    • 近接する同名のGTFS停留所(方向ポール)を同一の物理停留所として統合するようになりました。
    • 統合された停留所には、安定した共通の駅コードが割り当てられます。
  • バグ修正

    • 同名でも離れた場所にある停留所は、別の停留所として正しく扱われます。

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>
@TinyKitten TinyKitten self-assigned this Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

GTFS停留所の名称と距離を用いて近接ポールを統合し、代表stop_id由来のstation_g_cdをstation追加処理へ反映します。統合結果のログ出力と、近距離・遠距離・代表IDの安定性を検証するテストも追加しています。

Changes

GTFS停留所グルーピング

Layer / File(s) Summary
停留所グループマップの構築
stationapi/src/import.rs
距離計算を導入し、同名かつ指定半径内のポールをunion-findで統合します。代表stop_idは辞書順最小とし、近距離・遠距離・代表IDの安定性をテストします。
station_g_cd割り当てへの統合
stationapi/src/import.rs
integrate_gtfs_stops_to_stationsが統合マップを使用し、未登録stop_idには従来の生成処理をフォールバックします。統合グループ数もログ出力します。

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は従来方式で割り当てる
Loading

Poem

ぴょんと並んだポールたち
近けりゃひとつに、遠けりゃ別々
いちばん小さなIDを旗にして
駅コードを仲よく分けるよ
テストも跳ねて、確認完了!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 変更内容の中心であるGTFS停留所のグループ化と重複表示修正を簡潔に表しています。
Description check ✅ Passed 概要、変更の種類、変更内容、テストが記載されており、テンプレートの主要項目を満たしています。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 fix/bus-stop-pole-grouping

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

@github-actions github-actions Bot added fix 直した deploy-dev and removed fix 直した labels Jul 12, 2026
…ping

# Conflicts:
#	stationapi/src/import.rs
@github-actions github-actions Bot added the fix 直した label Jul 12, 2026

@coderabbitai coderabbitai Bot 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8251c5a and ec08f9f.

📒 Files selected for processing (1)
  • stationapi/src/import.rs

Comment thread stationapi/src/import.rs
Comment on lines +2288 to +2300
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;
}
}

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.

🎯 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.

Comment thread stationapi/src/import.rs
Comment on lines +2288 to +2302
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;
}
}
}
}

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.

🚀 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

Comment thread stationapi/src/import.rs
Comment on lines +3002 to +3004
// 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);

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.

🎯 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.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant