From 9b900988cdb8a6318b0c430c8d53ef598b5975e9 Mon Sep 17 00:00:00 2001
From: Tsubasa SEKIGUCHI <oss@tinykitten.me>
Date: Tue, 21 Oct 2025 19:36:59 +0900
Subject: [PATCH 1/2] =?UTF-8?q?GetRoutesMinimal=20RPC=E3=82=92=E5=AE=9F?=
 =?UTF-8?q?=E8=A3=85=20(#1334)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Add minimal route retrieval functionality and related proto types

* Refactor code formatting for improved readability in grpc.rs and query.rs

* Add line symbols to LineMinimal in get_lines_by_station_group_id

* Update subproject commit reference in stationapi/proto

* Add get_lines_by_id_list method and update QueryUseCase trait

* Refactor import order for improved readability in grpc.rs

* Use entry method for inserting line_minimal in all_lines to avoid overwriting

* Refactor route_row_tree_map creation into a separate build_route_tree_map method for improved readability

* Enhance line symbol handling in QueryInteractor: populate line_symbols for all lines and update line_minimal entries conditionally

* Refactor line symbol handling in QueryInteractor: update all_lines conditionally based on line_symbols presence

* Add AGENTS.md for automation agent and contributor workflow guidelines

* Update AGENTS.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---
 AGENTS.md                                     |  65 ++++++
 README.md                                     |   4 +
 stationapi/build.rs                           |  16 ++
 stationapi/proto                              |   2 +-
 .../src/presentation/controller/grpc.rs       |  41 +++-
 stationapi/src/use_case/interactor/query.rs   | 209 +++++++++++++++---
 stationapi/src/use_case/traits/query.rs       |   8 +-
 7 files changed, 305 insertions(+), 40 deletions(-)
 create mode 100644 AGENTS.md

diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..f09a70e4
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,65 @@
+# StationAPI Repository Guidelines
+
+This guide explains how automation agents and human contributors should work with the StationAPI repository so releases stay predictable, auditable, and safe. Update this file whenever you change the workflow or behavior it documents.
+
+## Project Layout
+- `stationapi/src/domain/` – Entity definitions and repository abstractions. The `entity/` module mirrors the gRPC schema, `repository/` provides `async_trait`-based interfaces, and `normalize.rs` contains text normalization for search.
+- `stationapi/src/use_case/` – Application logic. `interactor/query.rs` implements the `QueryUseCase` contract defined in `traits/query.rs`.
+- `stationapi/src/infrastructure/` – SQLx repositories for PostgreSQL. `My*Repository` types share an `Arc<PgPool)` and integration tests live behind `#[cfg_attr(not(feature = "integration-tests"), ignore)]`.
+- `stationapi/src/presentation/` – gRPC presentation layer. `presentation/controller/grpc.rs` wires `MyApi` to the generated server types. Health checks and reflection are enabled through `tonic-health` and `tonic-reflection`.
+- `stationapi/proto/stationapi.proto` – The gRPC contract. `build.rs` uses `tonic-build` to generate server code and `FILE_DESCRIPTOR_SET`.
+- `data/` – Canonical CSV datasets and schema definition (`create_table.sql`). Files follow the `N!table.csv` naming scheme to control import order. Detailed instructions are in `data/README.md`.
+- `data_validator/` – CLI that verifies cross-file constraints (`cargo run -p data_validator`).
+- `docker/` & `compose.yml` – Container definitions for the API and PostgreSQL 18. `docker/postgres/00_extensions.sql` ensures `pg_trgm` and `btree_gist` are installed.
+- `Makefile` – Convenience targets for running tests (`make test-unit`, `make test-integration`, `make test-all`).
+
+## Tooling and Environment
+- Rust: Use the stable toolchain (`rustup default stable`). Docker images build with `rust:1`.
+- Protobuf: `protoc` must exist when building locally (the Docker image installs it automatically).
+- PostgreSQL: Version 15+ with `pg_trgm` and `btree_gist` extensions. Compose spins up PostgreSQL 18 with these extensions preloaded.
+- Required environment variables:
+  - `DATABASE_URL` – SQLx connection string (e.g., `postgres://stationapi:stationapi@localhost/stationapi`).
+  - `DISABLE_GRPC_WEB` – `false` enables gRPC-Web; set to `true` for pure gRPC/HTTP2.
+  - `HOST` and `PORT` – listen address (defaults to `[::1]:50051`; Docker uses `0.0.0.0:50051`).
+  - `.env.test` exports `TEST_DATABASE_URL`, `RUST_LOG`, `RUST_BACKTRACE`, and `RUST_TEST_THREADS=1` for integration tests.
+- Recommended: copy `.env` to `.env.local`, override local values, and rely on `dotenv::from_filename(".env.local")` during startup.
+
+## Running and Deploying
+- **Local development**
+  1. Prepare PostgreSQL and set `DATABASE_URL`.
+  2. `cargo run -p stationapi` rebuilds the schema from `data/create_table.sql`, imports every `data/*.csv`, and then boots the gRPC server. If extension creation fails, grant the needed privileges manually.
+  3. Health checks respond to `grpc.health.v1.Health/Check`; gRPC Reflection is available for tooling such as `grpcurl`.
+- **Docker / Compose**
+  - `docker compose up --build` launches both the API and PostgreSQL containers. Source code mounts into `/app`, but hot reload is not provided; rebuild after code changes.
+  - Production images rely on `docker/api/Dockerfile`, which runs `cargo build -p stationapi --release` and copies `data/`.
+- **gRPC-Web**
+  - The server accepts HTTP/1.1 and wraps handlers via `tonic_web::enable`. Disable this behavior by exporting `DISABLE_GRPC_WEB=true` to require HTTP/2 clients only.
+
+## Data Management
+- CSV import order depends on the numeric prefix (`1!`, `2!`, ...). When adding datasets, choose a prefix that preserves foreign-key dependencies.
+- `data/create_table.sql` drops and recreates tables, indexes, and foreign keys. Update this script alongside any schema or CSV column changes.
+- `data_validator` currently verifies that `5!station_station_types.csv` references valid station and type IDs. Extend the validator when new cross-references are introduced and keep the process fail-fast (panic on invalid data).
+
+## Testing and Quality
+- **Unit tests** – `cargo test --lib --package stationapi` or `make test-unit`; focus on entities and repository mocks without a database.
+- **Integration tests** – `source .env.test && cargo test --lib --package stationapi --features integration-tests` or `make test-integration`. Use a dedicated schema behind `TEST_DATABASE_URL` and clean it up after runs.
+- **Full suite** – `make test-all` runs unit then integration tests sequentially. Set `RUST_LOG=debug` to inspect SQL queries during debugging.
+- **Linting and formatting** – Run `cargo fmt` and `cargo clippy --all-targets --all-features` before committing. Resolve new Clippy warnings unless an existing `#![allow]` covers the case.
+- **Data verification** – Execute `cargo run -p data_validator` whenever CSVs change and record results in pull requests.
+
+## gRPC Endpoint Overview
+- **Stations** – `GetStationById`, `GetStationByIdList`, `GetStationsByGroupId`, `GetStationsByCoordinates`, `GetStationsByLineId`, `GetStationsByName`, `GetStationsByLineGroupId`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types.
+- **Lines** – `GetLineById`, `GetLineByIdList`, `GetLinesByName`. Results include company data and computed line symbols based on repository helpers.
+- **Routes** – `GetRoutes`, `GetRoutesMinimal`. The minimal variant returns `RouteMinimalResponse` with deduplicated `LineMinimal` data; paging tokens are currently empty (pagination not implemented).
+- **Train types** – `GetTrainTypesByStationId`, `GetRouteTypes`. Train types aggregate by line group and include related lines plus optional train type metadata.
+- **Connected routes** – `GetConnectedRoutes`. `QueryInteractor::get_connected_stations` is not implemented yet and returns an empty vector; update the use-case and infrastructure layers together when adding real logic.
+- Changes to the service contract require coordinated updates to `proto/stationapi.proto`, regenerated code via `tonic-build`, and corresponding adjustments in both presentation and use-case layers.
+
+## Contribution Guidelines
+- Document the commands you executed (for example, ``cargo fmt && cargo clippy --all-targets --all-features && make test-unit``) and their outcomes in every pull request.
+- For database, gRPC, or schema updates, add architectural notes under `docs/` and synchronize README references so onboarding materials stay accurate.
+- When modifying `QueryInteractor`, ensure the enrichment steps (companies, train types, line symbols) still behave as expected. Double-check helper methods such as `update_station_vec_with_attributes` and `build_route_tree_map`.
+- Introducing new tables, endpoints, or feature flags must come with matching updates to this document and any other affected guidance.
+
+## Maintenance
+Keep this guide aligned with the repository. If a workflow, environment requirement, or endpoint changes, update AGENTS.md in the same pull request so automation agents and contributors work from current instructions.
diff --git a/README.md b/README.md
index 3b06114e..bbc169a1 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,10 @@
 
 The gRPC-Web API for provides nearby japanese train station.
 
+## Documentation
+
+- For automation agent and contributor workflows, see [AGENTS.md](AGENTS.md).
+
 ## Data Contribution
 
 This project includes a comprehensive dataset of Japanese railway information in the `data/` directory. The data is maintained in CSV format and contributions are primarily targeted at Japanese speakers. For detailed information about data structure and contribution guidelines, please refer to [data/README.md](data/README.md).
diff --git a/stationapi/build.rs b/stationapi/build.rs
index d53631d8..3c421963 100644
--- a/stationapi/build.rs
+++ b/stationapi/build.rs
@@ -25,6 +25,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
             "StopCondition",
             "#[derive(serde::Serialize, serde::Deserialize)]",
         )
+        .type_attribute(
+            "StationMinimal",
+            "#[derive(serde::Serialize, serde::Deserialize)]",
+        )
+        .type_attribute(
+            "LineMinimal",
+            "#[derive(serde::Serialize, serde::Deserialize)]",
+        )
+        .type_attribute(
+            "RouteMinimal",
+            "#[derive(serde::Serialize, serde::Deserialize)]",
+        )
+        .type_attribute(
+            "RouteMinimalResponse",
+            "#[derive(serde::Serialize, serde::Deserialize)]",
+        )
         .protoc_arg("--experimental_allow_proto3_optional")
         .file_descriptor_set_path(out_dir.join("stationapi_descriptor.bin"))
         .compile_protos(&["proto/stationapi.proto"], &["proto"])?;
diff --git a/stationapi/proto b/stationapi/proto
index 771b3ba7..9960d525 160000
--- a/stationapi/proto
+++ b/stationapi/proto
@@ -1 +1 @@
-Subproject commit 771b3ba7f68baeb0544679ca2760dc94f352f3ee
+Subproject commit 9960d5259686991d2bbad39d7f3f9aaf2bbf6485
diff --git a/stationapi/src/presentation/controller/grpc.rs b/stationapi/src/presentation/controller/grpc.rs
index 34ece77f..2dafd0e5 100644
--- a/stationapi/src/presentation/controller/grpc.rs
+++ b/stationapi/src/presentation/controller/grpc.rs
@@ -5,13 +5,13 @@ use crate::{
     },
     presentation::error::PresentationalError,
     proto::{
-        station_api_server::StationApi, GetConnectedStationsRequest, GetLineByIdRequest,
-        GetLinesByNameRequest, GetRouteRequest, GetStationByCoordinatesRequest,
+        station_api_server::StationApi, GetConnectedStationsRequest, GetLineByIdListRequest,
+        GetLineByIdRequest, GetLinesByNameRequest, GetRouteRequest, GetStationByCoordinatesRequest,
         GetStationByGroupIdRequest, GetStationByIdListRequest, GetStationByIdRequest,
         GetStationByLineIdRequest, GetStationsByLineGroupIdRequest, GetStationsByNameRequest,
         GetTrainTypesByStationIdRequest, MultipleLineResponse, MultipleStationResponse,
-        MultipleTrainTypeResponse, Route, RouteResponse, RouteTypeResponse, SingleLineResponse,
-        SingleStationResponse,
+        MultipleTrainTypeResponse, Route, RouteMinimalResponse, RouteResponse, RouteTypeResponse,
+        SingleLineResponse, SingleStationResponse,
     },
     use_case::{interactor::query::QueryInteractor, traits::query::QueryUseCase},
 };
@@ -214,6 +214,21 @@ impl StationApi for MyApi {
         }
     }
 
+    async fn get_routes_minimal(
+        &self,
+        request: tonic::Request<GetRouteRequest>,
+    ) -> Result<tonic::Response<RouteMinimalResponse>, tonic::Status> {
+        let from_id = request.get_ref().from_station_group_id;
+        let to_id = request.get_ref().to_station_group_id;
+
+        match self.query_use_case.get_routes_minimal(from_id, to_id).await {
+            Ok(response) => {
+                return Ok(Response::new(response));
+            }
+            Err(err) => Err(PresentationalError::from(err).into()),
+        }
+    }
+
     async fn get_route_types(
         &self,
         request: tonic::Request<GetRouteRequest>,
@@ -256,6 +271,24 @@ impl StationApi for MyApi {
         }))
     }
 
+    async fn get_line_by_id_list(
+        &self,
+        request: tonic::Request<GetLineByIdListRequest>,
+    ) -> Result<tonic::Response<MultipleLineResponse>, tonic::Status> {
+        let line_ids = &request.get_ref().line_ids;
+
+        let lines = match self.query_use_case.get_lines_by_id_vec(line_ids).await {
+            Ok(lines) => lines,
+            Err(err) => {
+                return Err(PresentationalError::OtherError(anyhow::anyhow!(err).into()).into())
+            }
+        };
+
+        Ok(Response::new(MultipleLineResponse {
+            lines: lines.into_iter().map(|line| line.into()).collect(),
+        }))
+    }
+
     async fn get_lines_by_name(
         &self,
         request: tonic::Request<GetLinesByNameRequest>,
diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs
index e9423c9a..ccf296b6 100644
--- a/stationapi/src/use_case/interactor/query.rs
+++ b/stationapi/src/use_case/interactor/query.rs
@@ -526,17 +526,7 @@ where
             .get_route_stops(from_station_id, to_station_id)
             .await?;
 
-        let route_row_tree_map: BTreeMap<i32, Vec<Station>> = stops.iter().fold(
-            BTreeMap::new(),
-            |mut acc: BTreeMap<i32, Vec<Station>>, value| {
-                if let Some(line_group_cd) = value.line_group_cd {
-                    acc.entry(line_group_cd).or_default().push(value.clone());
-                } else {
-                    acc.entry(value.line_cd).or_default().push(value.clone());
-                };
-                acc
-            },
-        );
+        let route_row_tree_map = self.build_route_tree_map(&stops);
 
         let mut routes: Vec<Route> = Vec::new();
 
@@ -551,34 +541,51 @@ where
                 .get_by_line_group_id_vec_for_routes(&line_group_id_vec)
                 .await?;
 
+            // Add line_symbols to all lines first
+            for line in tt_lines.iter_mut() {
+                line.line_symbols = self.get_line_symbols(line);
+            }
+
             let stops = stops
                 .iter()
                 .map(|row| {
                     let extracted_line = self.extract_line_from_station(row);
 
-                    if let Some(tt_line) =
-                        tt_lines.iter_mut().find(|line| line.line_cd == row.line_cd)
+                    if let Some(tt_line) = tt_lines.iter().find(|line| line.line_cd == row.line_cd)
                     {
-                        tt_line.line_symbols = self.get_line_symbols(tt_line);
-
                         let train_type = match row.type_id.is_some() {
-                            true => Some(Box::new(TrainType {
-                                id: row.type_id,
-                                station_cd: Some(row.station_cd),
-                                type_cd: row.type_cd,
-                                line_group_cd: row.line_group_cd,
-                                pass: row.pass,
-                                type_name: row.type_name.clone().unwrap_or_default(),
-                                type_name_k: row.type_name_k.clone().unwrap_or_default(),
-                                type_name_r: row.type_name_r.clone(),
-                                type_name_zh: row.type_name_zh.clone(),
-                                type_name_ko: row.type_name_ko.clone(),
-                                color: row.color.clone().unwrap_or_default(),
-                                direction: row.direction,
-                                kind: row.kind,
-                                line: Some(Box::new(tt_line.clone())),
-                                lines: tt_lines.to_vec(),
-                            })),
+                            true => {
+                                // Filter lines to only include those with matching line_group_cd
+                                // and remove duplicates by line_cd
+                                let mut seen_line_cds = std::collections::HashSet::new();
+                                let filtered_lines: Vec<Line> = tt_lines
+                                    .iter()
+                                    .filter(|line| {
+                                        row.line_group_cd.is_some()
+                                            && line.line_group_cd == row.line_group_cd
+                                            && seen_line_cds.insert(line.line_cd)
+                                    })
+                                    .cloned()
+                                    .collect();
+
+                                Some(Box::new(TrainType {
+                                    id: row.type_id,
+                                    station_cd: Some(row.station_cd),
+                                    type_cd: row.type_cd,
+                                    line_group_cd: row.line_group_cd,
+                                    pass: row.pass,
+                                    type_name: row.type_name.clone().unwrap_or_default(),
+                                    type_name_k: row.type_name_k.clone().unwrap_or_default(),
+                                    type_name_r: row.type_name_r.clone(),
+                                    type_name_zh: row.type_name_zh.clone(),
+                                    type_name_ko: row.type_name_ko.clone(),
+                                    color: row.color.clone().unwrap_or_default(),
+                                    direction: row.direction,
+                                    kind: row.kind,
+                                    line: Some(Box::new(tt_line.clone())),
+                                    lines: filtered_lines,
+                                }))
+                            }
                             false => None,
                         };
 
@@ -609,6 +616,108 @@ where
         Ok(routes)
     }
 
+    async fn get_routes_minimal(
+        &self,
+        from_station_id: u32,
+        to_station_id: u32,
+    ) -> Result<proto::RouteMinimalResponse, UseCaseError> {
+        let stops = self
+            .station_repository
+            .get_route_stops(from_station_id, to_station_id)
+            .await?;
+
+        let route_row_tree_map = self.build_route_tree_map(&stops);
+
+        let mut routes: Vec<proto::RouteMinimal> = Vec::new();
+        let mut all_lines: std::collections::HashMap<u32, proto::LineMinimal> =
+            std::collections::HashMap::new();
+
+        for (id, stops) in route_row_tree_map.iter() {
+            let stops_minimal = stops
+                .iter()
+                .map(|row| {
+                    let extracted_line = self.extract_line_from_station(row);
+
+                    // Add line to the lines collection
+                    let line_symbols = self
+                        .get_line_symbols(&extracted_line)
+                        .into_iter()
+                        .map(|ls| proto::LineSymbol {
+                            symbol: ls.symbol,
+                            color: ls.color,
+                            shape: ls.shape,
+                        })
+                        .collect();
+
+                    let line_minimal = proto::LineMinimal {
+                        id: extracted_line.line_cd as u32,
+                        name_short: extracted_line.line_name,
+                        color: extracted_line.line_color_c.unwrap_or_default(),
+                        line_type: extracted_line.line_type.unwrap_or(0),
+                        line_symbols,
+                    };
+
+                    // Update line: prefer entries with non-empty line_symbols
+                    all_lines
+                        .entry(line_minimal.id)
+                        .and_modify(|existing| {
+                            // Update if new line has symbols and existing doesn't
+                            if !line_minimal.line_symbols.is_empty()
+                                && existing.line_symbols.is_empty()
+                            {
+                                *existing = line_minimal.clone();
+                            }
+                        })
+                        .or_insert(line_minimal);
+
+                    // Create station minimal
+                    let station_numbers = self
+                        .get_station_numbers(row)
+                        .into_iter()
+                        .map(|sn| proto::StationNumber {
+                            line_symbol: sn.line_symbol,
+                            line_symbol_color: sn.line_symbol_color,
+                            line_symbol_shape: sn.line_symbol_shape,
+                            station_number: sn.station_number,
+                        })
+                        .collect();
+
+                    proto::StationMinimal {
+                        id: row.station_cd as u32,
+                        group_id: row.station_g_cd as u32,
+                        name: row.station_name.clone(),
+                        name_katakana: row.station_name_k.clone(),
+                        name_roman: row.station_name_r.clone(),
+                        line_ids: vec![extracted_line.line_cd as u32],
+                        station_numbers,
+                        stop_condition: row.pass.unwrap_or(0),
+                        has_train_types: Some(row.type_id.is_some()),
+                        train_type_id: row.type_id.map(|id| id as u32),
+                    }
+                })
+                .collect::<Vec<proto::StationMinimal>>();
+
+            // TODO: SQLで同等の処理を行う
+            let includes_requested_station = stops_minimal
+                .iter()
+                .any(|stop| stop.group_id == from_station_id || stop.group_id == to_station_id);
+            if !includes_requested_station {
+                continue;
+            }
+
+            routes.push(proto::RouteMinimal {
+                id: *id as u32,
+                stops: stops_minimal,
+            });
+        }
+
+        Ok(proto::RouteMinimalResponse {
+            routes,
+            lines: all_lines.into_values().collect(),
+            next_page_token: "".to_string(),
+        })
+    }
+
     async fn get_train_types(
         &self,
         from_station_id: u32,
@@ -633,11 +742,16 @@ where
             .get_by_line_group_id_vec(&line_group_id_vec)
             .await?;
 
-        let tt_lines = self
+        let mut tt_lines = self
             .line_repository
             .get_by_line_group_id_vec(&line_group_id_vec)
             .await?;
 
+        // Add line_symbols to all lines first
+        for line in tt_lines.iter_mut() {
+            line.line_symbols = self.get_line_symbols(line);
+        }
+
         for mut train_type in train_types.clone() {
             if result
                 .iter()
@@ -646,9 +760,13 @@ where
                 continue;
             }
 
+            let mut seen_line_cds = HashSet::new();
             train_type.lines = tt_lines
                 .iter()
-                .filter(|line| line.line_group_cd == train_type.line_group_cd)
+                .filter(|line| {
+                    line.line_group_cd == train_type.line_group_cd
+                        && seen_line_cds.insert(line.line_cd)
+                })
                 .map(|line| Line {
                     line_cd: line.line_cd,
                     company_cd: line.company_cd,
@@ -688,6 +806,10 @@ where
                     type_cd: line.type_cd,
                 })
                 .collect::<Vec<Line>>();
+
+            // Set the line field to the first line in the lines vector
+            train_type.line = train_type.lines.first().map(|l| Box::new(l.clone()));
+
             result.push(train_type);
         }
         Ok(result)
@@ -698,6 +820,11 @@ where
         Ok(line)
     }
 
+    async fn get_lines_by_id_vec(&self, line_ids: &[u32]) -> Result<Vec<Line>, UseCaseError> {
+        let lines = self.line_repository.get_by_ids(line_ids).await?;
+        Ok(lines)
+    }
+
     async fn get_lines_by_name(
         &self,
         line_name: String,
@@ -727,6 +854,20 @@ where
     TR: TrainTypeRepository,
     CR: CompanyRepository,
 {
+    fn build_route_tree_map(&self, stops: &[Station]) -> BTreeMap<i32, Vec<Station>> {
+        stops.iter().fold(
+            BTreeMap::new(),
+            |mut acc: BTreeMap<i32, Vec<Station>>, value| {
+                if let Some(line_group_cd) = value.line_group_cd {
+                    acc.entry(line_group_cd).or_default().push(value.clone());
+                } else {
+                    acc.entry(value.line_cd).or_default().push(value.clone());
+                };
+                acc
+            },
+        )
+    }
+
     fn build_station_from_row(
         &self,
         row: &Station,
diff --git a/stationapi/src/use_case/traits/query.rs b/stationapi/src/use_case/traits/query.rs
index 20f51457..9c5a85a6 100644
--- a/stationapi/src/use_case/traits/query.rs
+++ b/stationapi/src/use_case/traits/query.rs
@@ -5,7 +5,7 @@ use crate::{
         company::Company, line::Line, line_symbol::LineSymbol, station::Station,
         station_number::StationNumber, train_type::TrainType,
     },
-    proto::Route,
+    proto::{Route, RouteMinimalResponse},
     use_case::error::UseCaseError,
 };
 
@@ -79,12 +79,18 @@ pub trait QueryUseCase: Send + Sync + 'static {
         from_station_id: u32,
         to_station_id: u32,
     ) -> Result<Vec<Route>, UseCaseError>;
+    async fn get_routes_minimal(
+        &self,
+        from_station_id: u32,
+        to_station_id: u32,
+    ) -> Result<RouteMinimalResponse, UseCaseError>;
     async fn get_train_types(
         &self,
         from_station_id: u32,
         to_station_id: u32,
     ) -> Result<Vec<TrainType>, UseCaseError>;
     async fn find_line_by_id(&self, line_id: u32) -> Result<Option<Line>, UseCaseError>;
+    async fn get_lines_by_id_vec(&self, line_ids: &[u32]) -> Result<Vec<Line>, UseCaseError>;
     async fn get_lines_by_name(
         &self,
         line_name: String,

From 8c5d53a7a41bdbe821e41fcd10e94ddfe88830b5 Mon Sep 17 00:00:00 2001
From: Tsubasa SEKIGUCHI <oss@tinykitten.me>
Date: Sun, 16 Nov 2025 14:09:06 +0900
Subject: [PATCH 2/2] =?UTF-8?q?=E7=9F=B3=E6=A9=8B=E9=98=AA=E5=A4=A7?=
 =?UTF-8?q?=E5=89=8D=E9=A7=85=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=A9=E3=82=A4?=
 =?UTF-8?q?=E3=82=BA=E4=BF=AE=E6=AD=A3=20(#1336)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 data/3!stations.csv | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/data/3!stations.csv b/data/3!stations.csv
index 84007ab5..935f969b 100644
--- a/data/3!stations.csv
+++ b/data/3!stations.csv
@@ -7143,7 +7143,7 @@ station_cd,station_g_cd,station_name,station_name_k,station_name_r,station_name_
 3400208,3400208,岡町,オカマチ,Okamachi,Okamachi,冈町,오카마치,45,,,,,34002,27,561-0881,大阪府豊中市中桜塚１-１-１,135.465103,34.778949,0000-00-00,0000-00-00,0,3400208
 3400209,3400209,豊中,トヨナカ,Toyonaka,Toyonaka,丰中,도요나카,46,,,,,34002,27,560-0021,大阪府豊中市本町１-１-１,135.462073,34.787248,0000-00-00,0000-00-00,0,3400209
 3400210,3400210,蛍池,ホタルガイケ,Hotarugaike,Hotarugaike,萤池,호타루가이케,47,,,,,34002,27,560-0033,大阪府豊中市蛍池中町３-１,135.449238,34.794365,0000-00-00,0000-00-00,0,3400210
-3400211,3400211,石橋阪大前,イシバシハンダイマエ,Ishibashi handai-mae,Ishibashi handai-mae,石桥,이시바시,48,,,,,34002,27,563-0032,大阪府池田市石橋２-１８-１,135.445487,34.808254,0000-00-00,0000-00-00,0,3400211
+3400211,3400211,石橋阪大前,イシバシハンダイマエ,Ishibashi handai-mae,Ishibashi handai-mae,石桥阪大前,이시바시한다이마에,48,,,,,34002,27,563-0032,大阪府池田市石橋２-１８-１,135.445487,34.808254,0000-00-00,0000-00-00,0,3400211
 3400212,3400212,池田,イケダ,Ikeda,Ikeda,池田,이케다,49,,,,,34002,27,563-0056,大阪府池田市栄町１-１,135.425797,34.821544,0000-00-00,0000-00-00,0,3400212
 3400213,1162908,川西能勢口,カワニシノセグチ,Kawanishi-noseguchi,Kawanishi-noseguchi,川西能势口,가와니시노세구치,50,,,,,34002,28,666-0015,兵庫県川西市小花１-１-１０,135.413393,34.827654,0000-00-00,0000-00-00,0,3400213
 3400214,3400214,雲雀丘花屋敷,ヒバリガオカハナヤシキ,Hibarigaoka-hanayashiki,Hibarigaoka-hanayashiki,云雀丘花屋敷,히바리가오카하나야시키,51,,,,,34002,28,665-0805,兵庫県宝塚市雲雀丘１-１-１０,135.402946,34.827357,0000-00-00,0000-00-00,0,3400214
