From 898c167a0e15bc5888fabcdf70993012a00b17e2 Mon Sep 17 00:00:00 2001
From: Tsubasa SEKIGUCHI <oss@tinykitten.me>
Date: Thu, 5 Mar 2026 22:41:18 +0000
Subject: [PATCH] fix: replace line-name suffixes with English IPA instead of
 stripping
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Instead of just removing 線/本線/支線 from the IPA, replace them with
their English IPA equivalents (laɪn / meɪn laɪn) so that Google TTS
correctly pronounces the line type in English.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 stationapi/src/domain/ipa.rs        | 63 ++++++++++++++++-------------
 stationapi/src/use_case/dto/line.rs |  9 +++--
 2 files changed, 41 insertions(+), 31 deletions(-)

diff --git a/stationapi/src/domain/ipa.rs b/stationapi/src/domain/ipa.rs
index 1c204954..143370a8 100644
--- a/stationapi/src/domain/ipa.rs
+++ b/stationapi/src/domain/ipa.rs
@@ -1,25 +1,32 @@
-/// Common katakana suffixes for line names, ordered longest-first for greedy matching.
-const LINE_NAME_SUFFIXES: &[&str] = &["ホンセン", "シセン", "セン"];
-/// Suffixes that should NOT be stripped even though they end with セン.
+/// Katakana line-name suffixes paired with their English IPA replacements.
+/// Ordered longest-first for greedy matching.
+const LINE_NAME_SUFFIX_MAP: &[(&str, &str)] = &[
+    ("ホンセン", " meɪn laɪn"),
+    ("シセン", " laɪn"),
+    ("セン", " laɪn"),
+];
+/// Suffixes that should NOT be replaced even though they end with セン.
 const LINE_NAME_SUFFIX_EXCEPTIONS: &[&str] = &["シンカンセン"];
 
-/// Strip a common line-name suffix (線/本線/支線) from a katakana string.
+/// Replace a common line-name suffix (線/本線/支線) in a katakana string
+/// with its English IPA equivalent (Line / Main Line).
 /// 新幹線 (Shinkansen) is preserved as it is used as-is in English.
-/// Returns the stem (without the suffix). If no known suffix is found, returns the input unchanged.
-pub fn strip_line_name_suffix(input: &str) -> &str {
+/// Returns the stem and the English IPA suffix to append.
+/// If no known suffix is found, returns the full input with an empty suffix.
+pub fn replace_line_name_suffix(input: &str) -> (&str, &str) {
     for exception in LINE_NAME_SUFFIX_EXCEPTIONS {
         if input.ends_with(exception) {
-            return input;
+            return (input, "");
         }
     }
-    for suffix in LINE_NAME_SUFFIXES {
+    for (suffix, replacement) in LINE_NAME_SUFFIX_MAP {
         if let Some(stem) = input.strip_suffix(suffix) {
             if !stem.is_empty() {
-                return stem;
+                return (stem, replacement);
             }
         }
     }
-    input
+    (input, "")
 }
 
 /// Convert a katakana string to its IPA transcription.
@@ -634,54 +641,54 @@ mod tests {
     }
 
     // ============================================
-    // strip_line_name_suffix tests
+    // replace_line_name_suffix tests
     // ============================================
 
     #[test]
-    fn test_strip_sen() {
+    fn test_replace_sen() {
         assert_eq!(
-            strip_line_name_suffix("セイブイケブクロセン"),
-            "セイブイケブクロ"
+            replace_line_name_suffix("セイブイケブクロセン"),
+            ("セイブイケブクロ", " laɪn")
         );
     }
 
     #[test]
-    fn test_strip_honsen() {
+    fn test_replace_honsen() {
         assert_eq!(
-            strip_line_name_suffix("トウカイドウホンセン"),
-            "トウカイドウ"
+            replace_line_name_suffix("トウカイドウホンセン"),
+            ("トウカイドウ", " meɪn laɪn")
         );
     }
 
     #[test]
-    fn test_strip_shinkansen_preserved() {
+    fn test_replace_shinkansen_preserved() {
         // 新幹線(Shinkansen)は英語でもそのまま使われるので除去しない
         assert_eq!(
-            strip_line_name_suffix("トウホクシンカンセン"),
-            "トウホクシンカンセン"
+            replace_line_name_suffix("トウホクシンカンセン"),
+            ("トウホクシンカンセン", "")
         );
     }
 
     #[test]
-    fn test_strip_shisen() {
+    fn test_replace_shisen() {
         assert_eq!(
-            strip_line_name_suffix("ナガノハラクサツグチシセン"),
-            "ナガノハラクサツグチ"
+            replace_line_name_suffix("ナガノハラクサツグチシセン"),
+            ("ナガノハラクサツグチ", " laɪn")
         );
     }
 
     #[test]
-    fn test_strip_no_suffix() {
+    fn test_replace_no_suffix() {
         // ライン等セン以外の末尾はそのまま返す
         assert_eq!(
-            strip_line_name_suffix("ショウナンシンジュクライン"),
-            "ショウナンシンジュクライン"
+            replace_line_name_suffix("ショウナンシンジュクライン"),
+            ("ショウナンシンジュクライン", "")
         );
     }
 
     #[test]
-    fn test_strip_bare_sen_returns_unchanged() {
+    fn test_replace_bare_sen_returns_unchanged() {
         // "セン" だけの場合、stemが空になるので除去しない
-        assert_eq!(strip_line_name_suffix("セン"), "セン");
+        assert_eq!(replace_line_name_suffix("セン"), ("セン", ""));
     }
 }
diff --git a/stationapi/src/use_case/dto/line.rs b/stationapi/src/use_case/dto/line.rs
index 734e65c7..79a0f438 100644
--- a/stationapi/src/use_case/dto/line.rs
+++ b/stationapi/src/use_case/dto/line.rs
@@ -1,15 +1,18 @@
 use crate::{
     domain::{
         entity::{gtfs::TransportType, line::Line},
-        ipa::{katakana_to_ipa, strip_line_name_suffix},
+        ipa::{katakana_to_ipa, replace_line_name_suffix},
     },
     proto::{Line as GrpcLine, TransportType as GrpcTransportType},
 };
 
 impl From<Line> for GrpcLine {
     fn from(line: Line) -> Self {
-        let name_ipa = katakana_to_ipa(strip_line_name_suffix(&line.line_name_k))
-            .filter(|ipa| !ipa.is_empty());
+        let name_ipa = {
+            let (stem, suffix_ipa) = replace_line_name_suffix(&line.line_name_k);
+            katakana_to_ipa(stem).map(|ipa| format!("{ipa}{suffix_ipa}"))
+        }
+        .filter(|ipa| !ipa.is_empty());
         // バス路線の場合は line_type を OtherLineType (0) に強制
         // (鉄道用の line_type が誤って設定されている可能性があるため)
         let line_type = if line.transport_type == TransportType::Bus {
