From 3324b6112ea60053aa54fda10d3a3a0f1b23110f Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Wed, 12 Aug 2026 09:22:21 +0900 Subject: [PATCH] =?UTF-8?q?IPA=E8=BE=9E=E6=9B=B8=E3=82=92=E6=8B=A1?= =?UTF-8?q?=E5=85=85=E3=81=97=E3=81=A6=E3=83=AD=E3=83=BC=E3=83=9E=E5=AD=97?= =?UTF-8?q?=E9=A7=85=E5=90=8D=E3=81=AE=E6=9C=AA=E8=A7=A3=E6=B1=BA=E3=82=92?= =?UTF-8?q?61=E4=BB=B6=E3=81=8B=E3=82=891=E4=BB=B6=E3=81=AB=E5=89=8A?= =?UTF-8?q?=E6=B8=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make ipa-audit が報告する未解決名 61 件 (lines 1 / stations 59 / train_types 1) を解消する。 - lookup_english_word_ipa に 60 語を追加する。英語・カタカナ外来語由来の語 (Disney / Resort / Bayside / Screen / Vogel など) と、romaji_to_katakana が解釈できない訓令式・非ヘボン式表記 (Sirokanedai / Tamati / Mitumatu / Hatchonawate / 3chōme など) の両方を対象とする。日本語読みの IPA は katakana_to_ipa() の出力に合わせ、長音のみ既存エントリと同じ ː 表記に畳んだ。 - 日本式ローマ字のサーカムフレックス長音 (â/î/û/ê/ô) をトークン文字として 受け付け、マクロンと同じ規則で母音列へ展開する。従来は区切り文字扱いのため "Kyûjômae" が "Ky" / "j" / "mae" に割れて読みが崩れていた。 - camelCase 分割で現れる "sea" を追加する。"DisneySea" が「セア」と誤読されて いたが、名前全体としては解決扱いのため監査に現れないケースだった。 CSV 全 12,094 名称の name_roman_ipa を変更前後で突き合わせ、差分 56 件が すべて意図した改善であることを確認済み。残る未解決 1 件は Kii-Hosoakwa で、 CSV 側の綴り誤り (正: Kii-Hosokawa) のため辞書では扱わない。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Px4SXdeBULB87KqD1TzKfy --- stationapi/src/domain/ipa.rs | 197 +++++++++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 6 deletions(-) diff --git a/stationapi/src/domain/ipa.rs b/stationapi/src/domain/ipa.rs index 37d3e441..582c580c 100644 --- a/stationapi/src/domain/ipa.rs +++ b/stationapi/src/domain/ipa.rs @@ -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" に割れて読みが崩れる。 + | 'Â' + | 'Î' + | 'Û' + | 'Ê' + | 'Ô' + | 'â' + | 'î' + | 'û' + | 'ê' + | 'ô' ) } @@ -411,13 +434,15 @@ fn normalize_name_token(token: &str) -> String { .to_lowercase() } +/// マクロン (ā/ī/ū/ē/ō) とサーカムフレックス (â/î/û/ê/ô) の長音表記を、 +/// romaji_to_katakana が解釈できる母音の並びへ展開する。 fn normalize_name_char(c: char) -> Vec { 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], } } @@ -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, } } @@ -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 = [