Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This guide explains how automation agents and human contributors should work wit
- **IPA coverage audit** – Execute `make ipa-audit` when English or romanized CSV names change. This is a read-only report for `data/2!lines.csv`, `data/3!stations.csv`, and `data/4!types.csv`; it does not fail validation, but highlights unresolved tokens and example names so the IPA dictionary can be extended deliberately.

## gRPC Endpoint Overview
- **Stations** – `GetStationById`, `GetStationByIdList`, `GetStationsByGroupId`, `GetStationsByCoordinates`, `GetStationsByLineId`, `GetStationsByName`, `GetStationsByLineGroupId`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types.
- **Stations** – `GetStationById`, `GetStationByIdList`, `GetStationsByGroupId`, `GetStationsByCoordinates`, `GetStationsByLineId`, `GetStationsByName`, `GetStationsByLineGroupId`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types. `GetStationsByLineId` resolves the line's local train-type group (rail `kind` 0/1 or a `priority > 0` type); when no such group exists — bus lines only carry `BusRoute` (`kind` 7, `priority` 0) variants — it falls back to the line's plain typeless station list so bus stop listings never return empty.
- **Lines** – `GetLineById`, `GetLinesByIdList`, `GetLinesByName`. Results include company data and computed line symbols based on repository helpers.
- **Routes** – `GetRoutes`, `GetRoutesMinimal`, `GetConnectedRoutes`. 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. Rail variants use `TrainTypeKind::{Default, Branch, Rapid, Express, LimitedExpress, HighSpeedRapid, CommuterRapid}` (0-6); bus variants use `BusRoute` (7), which represents a `(route_id, shape_id)` operation pattern (e.g. 循環 / 短ターン / 支線) generated automatically from the configured GTFS bus feeds (Toei Bus, Seibu Bus, Keio Bus) and the converted Tokyu Bus JSON.
Expand Down
2 changes: 1 addition & 1 deletion data/3!stations.csv
Original file line number Diff line number Diff line change
Expand Up @@ -7013,7 +7013,7 @@ station_cd,station_g_cd,station_name,station_name_k,station_name_r,station_name_
3200737,3200737,高野下,コウヤシタ,Konoshita,Konoshita,高野下,고야시타,81,,,,,32007,30,648-0141,和歌山県伊都郡九度山町大字椎出8-1,135.566359,34.271635,0000-00-00,0000-00-00,0,3200737
3200738,3200738,下古沢,シモコサワ,Shimokosawa,Shimokosawa,下古泽,시모코사와,82,,,,,32007,30,648-0145,和歌山県伊都郡九度山町大字下古沢,135.553207,34.266497,0000-00-00,0000-00-00,0,3200738
3200739,3200739,上古沢,カミコサワ,Kamikosawa,Kamikosawa,上古泽,가미코사와,83,,,,,32007,30,648-0143,和歌山県伊都郡九度山町大字上古沢,135.548025,34.257362,0000-00-00,0000-00-00,0,3200739
3200740,3200740,紀伊細川,キイホソカワ,Kii-Hosoakwa,Kii-Hosoakwa,纪伊细川,기이호소카와,84,,,,,32007,30,648-0262,和歌山県伊都郡高野町大字細川732,135.553705,34.235723,0000-00-00,0000-00-00,0,3200740
3200740,3200740,紀伊細川,キイホソカワ,Kii-Hosokawa,Kii-Hosokawa,纪伊细川,기이호소카와,84,,,,,32007,30,648-0262,和歌山県伊都郡高野町大字細川732,135.553705,34.235723,0000-00-00,0000-00-00,0,3200740
3200741,3200741,紀伊神谷,キイカミヤ,Kii-Kamiya,Kii-Kamiya,纪伊神谷,기이카미야,85,,,,,32007,30,648-0200,和歌山県伊都郡高野町大字神谷,135.571034,34.242852,0000-00-00,0000-00-00,0,3200741
3200742,3200742,極楽橋,ゴクラクバシ,Gokurakubashi,Gokurakubashi,极乐桥,고쿠라쿠바시,86,,,,,32007,30,648-0211,和歌山県伊都郡高野町大字高野山国有林第8林班,135.582078,34.23435,0000-00-00,0000-00-00,0,3200742
3200801,3200742,極楽橋,ゴクラクバシ,Gokurakubashi,Gokurakubashi,极乐桥,고쿠라쿠바시,86,,,,,32008,30,648-0211,和歌山県伊都郡高野町大字高野山国有林第8林班,135.582078,34.23435,0000-00-00,0000-00-00,0,3200801
Expand Down
2 changes: 2 additions & 0 deletions data/4!types.csv
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,5 @@ DEFAULT,538,モーニングライナー,モーニングライナー,Morning Line
DEFAULT,539,イブニングライナー,イブニングライナー,Evening Liner,Evening Liner,이브닝 라이너,#9400d3,0,4,0,,COMPLETE
DEFAULT,540,臨時ライナー,リンジライナー,Special Liner,临时Liner,임시 라이너,#add8e6,0,4,0,,COMPLETE
DEFAULT,541,GRAN 天空,グランテンクウ,GRAN Tenkū,GRAN 天空,GRAN 텐쿠,#AD7E2A,0,4,0,,COMPLETE
DEFAULT,542,みすず,ミスズ,Misuzu,水篶,미스즈,#DC143C,2,2,0,長野方面,COMPLETE
DEFAULT,543,みすず,ミスズ,Misuzu,水篶,미스즈,#DC143C,1,2,0,飯田方面,COMPLETE
106 changes: 106 additions & 0 deletions data/5!station_station_types.csv
Original file line number Diff line number Diff line change
Expand Up @@ -41124,3 +41124,109 @@ DEFAULT,1171629,125,1168,0,海田市
DEFAULT,1171630,125,1168,1,向洋
DEFAULT,1171631,125,1168,1,天神川
DEFAULT,1171632,125,1168,0,広島
DEFAULT,1141410,542,1169,0,飯田
DEFAULT,1141411,542,1169,1,桜町
DEFAULT,1141412,542,1169,1,伊那上郷
DEFAULT,1141413,542,1169,0,元善光寺
DEFAULT,1141414,542,1169,1,下市田
DEFAULT,1141415,542,1169,0,市田
DEFAULT,1141416,542,1169,1,下平
DEFAULT,1141417,542,1169,1,山吹
DEFAULT,1141418,542,1169,0,伊那大島
DEFAULT,1141419,542,1169,0,上片桐
DEFAULT,1141420,542,1169,1,伊那田島
DEFAULT,1141421,542,1169,1,高遠原
DEFAULT,1141422,542,1169,0,七久保
DEFAULT,1141423,542,1169,1,伊那本郷
DEFAULT,1141424,542,1169,0,飯島
DEFAULT,1141425,542,1169,1,田切
DEFAULT,1141426,542,1169,0,伊那福岡
DEFAULT,1141427,542,1169,0,小町屋
DEFAULT,1141428,542,1169,0,駒ケ根
DEFAULT,1141429,542,1169,0,大田切
DEFAULT,1141430,542,1169,0,宮田
DEFAULT,1141431,542,1169,0,赤木
DEFAULT,1141432,542,1169,0,沢渡
DEFAULT,1141433,542,1169,0,下島
DEFAULT,1141434,542,1169,0,伊那市
DEFAULT,1141435,542,1169,0,伊那北
DEFAULT,1141436,542,1169,0,田畑
DEFAULT,1141437,542,1169,0,北殿
DEFAULT,1141438,542,1169,0,木ノ下
DEFAULT,1141439,542,1169,0,伊那松島
DEFAULT,1141440,542,1169,0,沢
DEFAULT,1141441,542,1169,0,羽場
DEFAULT,1141442,542,1169,0,伊那新町
DEFAULT,1141443,542,1169,0,宮木
DEFAULT,1141444,542,1169,0,辰野
DEFAULT,1131150,542,1169,0,辰野
DEFAULT,1131149,542,1169,0,川岸
DEFAULT,1131147,542,1169,0,岡谷
DEFAULT,1131148,542,1169,0,みどり湖
DEFAULT,1131153,542,1169,0,塩尻
DEFAULT,1141201,542,1169,0,塩尻
DEFAULT,1141202,542,1169,0,広丘
DEFAULT,1141203,542,1169,0,村井
DEFAULT,1141215,542,1169,0,平田
DEFAULT,1141204,542,1169,0,南松本
DEFAULT,1141205,542,1169,0,松本
DEFAULT,1141206,542,1169,0,田沢
DEFAULT,1141207,542,1169,0,明科
DEFAULT,1141208,542,1169,0,西条
DEFAULT,1141209,542,1169,0,坂北
DEFAULT,1141210,542,1169,0,聖高原
DEFAULT,1141211,542,1169,0,冠着
DEFAULT,1141212,542,1169,0,姨捨
DEFAULT,1141213,542,1169,0,稲荷山
DEFAULT,1141214,542,1169,0,篠ノ井
DEFAULT,1140301,542,1169,0,篠ノ井
DEFAULT,1140302,542,1169,0,今井
DEFAULT,1140303,542,1169,0,川中島
DEFAULT,1140304,542,1169,0,安茂里
DEFAULT,1140305,542,1169,0,長野
DEFAULT,1141410,543,1170,0,飯田
DEFAULT,1141411,543,1170,0,桜町
DEFAULT,1141412,543,1170,0,伊那上郷
DEFAULT,1141413,543,1170,0,元善光寺
DEFAULT,1141414,543,1170,1,下市田
DEFAULT,1141415,543,1170,0,市田
DEFAULT,1141416,543,1170,1,下平
DEFAULT,1141417,543,1170,1,山吹
DEFAULT,1141418,543,1170,0,伊那大島
DEFAULT,1141419,543,1170,0,上片桐
DEFAULT,1141420,543,1170,1,伊那田島
DEFAULT,1141421,543,1170,1,高遠原
DEFAULT,1141422,543,1170,0,七久保
DEFAULT,1141423,543,1170,0,伊那本郷
DEFAULT,1141424,543,1170,0,飯島
DEFAULT,1141425,543,1170,1,田切
DEFAULT,1141426,543,1170,0,伊那福岡
DEFAULT,1141427,543,1170,0,小町屋
DEFAULT,1141428,543,1170,0,駒ケ根
DEFAULT,1141429,543,1170,1,大田切
DEFAULT,1141430,543,1170,0,宮田
DEFAULT,1141431,543,1170,1,赤木
DEFAULT,1141432,543,1170,0,沢渡
DEFAULT,1141433,543,1170,1,下島
DEFAULT,1141434,543,1170,0,伊那市
DEFAULT,1141435,543,1170,0,伊那北
DEFAULT,1141436,543,1170,1,田畑
DEFAULT,1141437,543,1170,0,北殿
DEFAULT,1141438,543,1170,0,木ノ下
DEFAULT,1141439,543,1170,0,伊那松島
DEFAULT,1141440,543,1170,0,沢
DEFAULT,1141441,543,1170,0,羽場
DEFAULT,1141442,543,1170,1,伊那新町
DEFAULT,1141443,543,1170,0,宮木
DEFAULT,1141444,543,1170,0,辰野
DEFAULT,1131150,543,1170,0,辰野
DEFAULT,1131149,543,1170,0,川岸
DEFAULT,1131147,543,1170,0,岡谷
DEFAULT,1131148,543,1170,0,みどり湖
DEFAULT,1131153,543,1170,0,塩尻
DEFAULT,1141201,543,1170,0,塩尻
DEFAULT,1141202,543,1170,0,広丘
DEFAULT,1141203,543,1170,0,村井
DEFAULT,1141215,543,1170,0,平田
DEFAULT,1141204,543,1170,0,南松本
DEFAULT,1141205,543,1170,0,松本
197 changes: 191 additions & 6 deletions stationapi/src/domain/ipa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,30 @@ fn is_name_token_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'\'' | '.' | 'Ā' | 'Ī' | 'Ū' | 'Ē' | 'Ō' | 'ā' | 'ī' | 'ū' | 'ē' | 'ō'
'\'' | '.'
| 'Ā'
| 'Ī'
| 'Ū'
| 'Ē'
| 'Ō'
| 'ā'
| 'ī'
| 'ū'
| 'ē'
| 'ō'
// 日本式ローマ字のサーカムフレックス長音 (Kyûjômae / Sessokyô / Okuôi)。
// トークン文字に含めないと長音記号が区切り文字扱いになり、
// "Kyûjômae" が "Ky" / "j" / "mae" に割れて読みが崩れる。
| 'Â'
| 'Î'
| 'Û'
| 'Ê'
| 'Ô'
| 'â'
| 'î'
| 'û'
| 'ê'
| 'ô'
)
}

Expand All @@ -411,13 +434,15 @@ fn normalize_name_token(token: &str) -> String {
.to_lowercase()
}

/// マクロン (ā/ī/ū/ē/ō) とサーカムフレックス (â/î/û/ê/ô) の長音表記を、
/// romaji_to_katakana が解釈できる母音の並びへ展開する。
fn normalize_name_char(c: char) -> Vec<char> {
match c {
'Ā' | 'ā' => vec!['a', 'a'],
'Ī' | 'ī' => vec!['i', 'i'],
'Ū' | 'ū' => vec!['u', 'u'],
'Ē' | 'ē' => vec!['e', 'i'],
'Ō' | 'ō' => vec!['o', 'u'],
'Ā' | 'ā' | 'Â' | 'â' => vec!['a', 'a'],
'Ī' | 'ī' | 'Î' | 'î' => vec!['i', 'i'],
'Ū' | 'ū' | 'Û' | 'û' => vec!['u', 'u'],
'Ē' | 'ē' | 'Ê' | 'ê' => vec!['e', 'i'],
'Ō' | 'ō' | 'Ô' | 'ô' => vec!['o', 'u'],
_ => vec![c],
}
}
Expand Down Expand Up @@ -685,6 +710,74 @@ fn lookup_english_word_ipa(word: &str) -> Option<&'static str> {
"t" => Some("tiː"),
"trans" => Some("tɹæns"),
"zoological" => Some("zuːəlɑdʒɪkəl"),
// ここから下は `make ipa-audit` で未解決として報告された語。
// 英語・カタカナ外来語由来のもの。
"bayside" => Some("beɪsaɪd"),
"disney" => Some("dɪzni"),
"disneyland" => Some("dɪznilænd"),
// "DisneySea" は camelCase 分割で "Disney" + "Sea" になる。"sea" を登録しないと
// romaji_to_katakana が「セア」と読んでしまい、監査には出ないまま誤読が残る。
"sea" => Some("siː"),
"girls'" => Some("gɝlz"),
"gran" => Some("gɹɑːn"),
"playpia" => Some("pleɪpiə"),
"portopia" => Some("pɔɹtoʊpiə"),
"pref.art" => Some("pɹɛf ɑɹt"),
"pref.sports" => Some("pɹɛf spɔɹts"),
"race" => Some("ɹeɪs"),
"resort" => Some("ɹɪzɔɹt"),
"river" => Some("ɹɪvɚ"),
"royce'" => Some("ɹɔɪs"),
"ruins" => Some("ɹuːɪnz"),
"screen" => Some("skɹiːn"),
"shop" => Some("ʃɑp"),
"socio" => Some("soʊʃioʊ"),
// 「ウェスパ椿山」の "WeSPa" は camelCase 分割で "We" + "SPa" に割れるため、
// 英単語の we (wiː) ではなくカタカナ読みに寄せた wɛ を割り当てる。
"we" => Some("wɛ"),
"spa" => Some("spɑː"),
"st" => Some("seɪnt"),
"tennis" => Some("tɛnɪs"),
"toyopet" => Some("toʊjoʊpɛt"),
"univ" => Some("juːnəvɚsəti"),
"vogel" => Some("foʊgəl"),
"yomiuriland" => Some("jomiɯɾilænd"),
"yrp" => Some("waɪ ɑɹ piː"),
// 訓令式・非ヘボン式のローマ字表記など、romaji_to_katakana が解釈できない日本語読み。
// IPA は katakana_to_ipa() の出力に合わせ、長音のみ既存エントリと同じ ː 表記に畳んでいる。
"3choume" => Some("sant͡ɕoːme"),
"4choume" => Some("jont͡ɕoːme"),
"daiunzi" => Some("daiɯɲdʑi"),
"etchuujima" => Some("ett͡ɕɯːdʑima"),
"golfjomae" => Some("gɑlf dʑoːmae"),
"hasirano" => Some("haɕiɾano"),
"hatcho" => Some("hatt͡ɕoː"),
"hatchome" => Some("hatt͡ɕoːme"),
"hatchonawate" => Some("hatt͡ɕoːnawate"),
"hatchoubori" => Some("hatt͡ɕoːboɾi"),
"hatcomuta" => Some("hatt͡ɕoːmɯta"),
"inadazutumi" => Some("inadadzɯt͡sɯmi"),
"inuboh" => Some("inɯboː"),
"itchumae" => Some("itt͡ɕɯːmae"),
"itihana" => Some("it͡ɕihana"),
"juitchome" => Some("dʑɯːitt͡ɕoːme"),
"kohjiro" => Some("koːdʑiɾo"),
"kozyou" => Some("kodʑoː"),
"mitumatu" => Some("mit͡sɯmat͡sɯ"),
"mitusawa" => Some("mit͡sɯsawa"),
"nijuhatchome" => Some("ɲidʑɯːhatt͡ɕoːme"),
"oh" => Some("oː"),
"ohda" => Some("oːda"),
"ohmisaki" => Some("oːmisakʲi"),
"ohshou" => Some("oːɕoː"),
"saidaizi" => Some("saidaidʑi"),
"saigoh" => Some("saigoː"),
"shicikenjaya" => Some("ɕit͡ɕikeɲdʑaja"),
"sirokanedai" => Some("ɕiɾokanedai"),
"sizuoka" => Some("ɕizɯoka"),
"suzi" => Some("sɯdʑi"),
"tamati" => Some("tamat͡ɕi"),
"tsudumigaura" => Some("t͡sɯdzɯmigaɯɾa"),
_ => None,
}
}
Expand Down Expand Up @@ -1552,6 +1645,98 @@ mod tests {
);
}

#[test]
fn test_station_name_ipa_supports_kunrei_style_romanization() {
// 訓令式 (si/ti/tu/zi) は romaji_to_katakana が解釈できないため辞書で補う。
assert_eq!(
station_name_to_ipa("シロカネダイ", Some("Sirokanedai")),
Some("ɕilokanedai".to_string())
);
assert_eq!(
station_name_to_ipa("タマチ", Some("Tamati")),
Some("tamat͡ɕi".to_string())
);
assert_eq!(
station_name_to_ipa("シンサイダイジチョウスジ", Some("Shin-saidaizi-cho-suzi")),
Some("ɕiɴ saidaidʑi t͡ɕo sɯdʑi".to_string())
);
}

#[test]
fn test_station_name_ipa_expands_circumflex_long_vowels() {
// サーカムフレックスをトークン文字として扱わないと "Ky" / "j" / "mae" に割れる。
assert_eq!(
station_name_to_ipa("キュウジョウマエ", Some("Kyûjômae")),
Some("kʲɯ.ɯdʑo.ɯma.e".to_string())
);
assert_eq!(
station_name_to_ipa("セッソキョウオンセン", Some("Sessokyô Onsen")),
Some("sessokʲo.ɯ onseɴ".to_string())
);
}

#[test]
fn test_station_name_ipa_supports_oh_style_long_vowels() {
// 「オウ」を oh と綴る表記 (犬吠 / 西郷 / 海の王迎)。
assert_eq!(
station_name_to_ipa("イヌボウ", Some("Inuboh")),
Some("inɯboː".to_string())
);
assert_eq!(
station_name_to_ipa("ウミノオウムカエ", Some("Umino-oh-mukae")),
Some("ɯmino oː mɯka.e".to_string())
);
}

#[test]
fn test_station_name_ipa_supports_chome_variants() {
assert_eq!(
station_name_to_ipa("ニシハッチョウメ", Some("Nishi hatchome")),
Some("ɲiɕi hatt͡ɕoːme".to_string())
);
assert_eq!(
station_name_to_ipa("テヅカヤマサンチョウメ", Some("Tezukayama-3chōme")),
Some("tezɯkajama sant͡ɕoːme".to_string())
);
}

#[test]
fn test_station_name_ipa_supports_facility_brand_words() {
assert_eq!(
station_name_to_ipa(
"トウキョウディズニーランド・ステーション",
Some("Tokyo Disneyland Station")
),
Some("tokʲo dɪznilænd steɪʃən".to_string())
);
// "DisneySea" は camelCase 分割で "Disney" + "Sea" になる。
assert_eq!(
station_name_to_ipa(
"トウキョウディズニーシー・ステーション",
Some("Tokyo DisneySea Station")
),
Some("tokʲo dɪzni siː steɪʃən".to_string())
);
}

#[test]
fn test_station_name_ipa_supports_abbreviated_english_words() {
// "St." / "Univ." は normalize_name_token が末尾のピリオドを落とす。
assert_eq!(
station_name_to_ipa("セイマリアビョウインマエ", Some("St. Mary's Hospital")),
Some("seɪnt mɛɹiz hɑspɪtəl".to_string())
);
assert_eq!(
station_name_to_ipa("アイダイイガクブミナミグチ", Some("Ehime Univ. Hospital")),
Some("eçime juːnəvɚsəti hɑspɪtəl".to_string())
);
// "Pref.Art" はピリオドが語中にあるため 1 トークンのまま引かれる。
assert_eq!(
station_name_to_ipa("ケンリツビジュツカンマエ", Some("Pref.Art Museum")),
Some("pɹɛf ɑɹt mjuːziəm".to_string())
);
}

#[test]
fn test_station_name_ipa_supports_meitetsu_prefixed_station_names_from_csv() {
let cases = [
Expand Down
Loading
Loading