ククログ

株式会社クリアコード > ククログ > 2026 > 続・Nominatimが広島平和記念資料館を見つけられない理由とその解決方法

続・Nominatimが広島平和記念資料館を見つけられない理由とその解決方法

2026年11月28日の11:15からFOSS4G Hokkaido 2026で発表予定の阿部です。 「続・Nominatimが広島平和記念資料館を見つけられない理由とその解決方法」というタイトルです。

FOSS4G Hiroshima 2026での発表の続きの内容です。 広島での発表ではPGroonga(PostgreSQLの拡張機能)を使うアプローチでしたが、発表の後に調べたところPGroongaなしでも改善できそうだったので、それについて発表します。

スライドも公開しますが、スライドだけ見ても説明不足なところがあるので、それを補足するための記事です。

(このあとスライドを公開してここにリンクを追加します。)

FOSS4G Hiroshima 2026の発表内容

詳しくは発表をまとめた記事をご覧ください。 その記事で説明している内容にも触れるので、先に読んでからこの記事を読んでもらった方がよりわかりやすいと思います。

かなりざっくりふりかえっておくと、

  • 日本語だといい感じに検索できない原因は2つ
    • 検索最適化が裏目に出る
    • 漢字は日本語でも中国語の読み
  • PGroongaで全文検索をして解決だ!

という感じです。

そのうち「検索最適化が裏目に出る」をPGroongaなどの追加なしに、データの再構築なしに、コードを数十行変えて解決できないか!?を検証したのが今回の発表です。

さっそく原因の解説

一言でいうと"「ペナルティ」がいい感じに設定されないから"です。 ということで、そもそも「ペナルティ」とは何か?いい感じに設定されないとはどういう状態か?を順に説明していきます。

発表の会場が北海道なので「北海道大学付属図書館」を例に使います。 これも「北海道大学付属図書館」では見つからないけど「北海道大学付属図書館本館」なら見つかる検索クエリーです。

また、このあと「ペナルティ」についての説明をしていきますが、わかりやすさ優先のため単純化しています。 ご了承ください。

前提: 検索クエリーは分割される

「ペナルティ」の説明の前に前提知識の確認で「クエリーも登録名も単語に分割して突き合わせる」に触れます。

Hokkaido University Libraryであれば、Hokkaido、University、Libraryと一度単語に区切ってそれを使って突き合わせます。 で、この分割した単語をどの組み合わせで検索するとよりよい結果が得られそうか、を「ペナルティ」を使って計算するわけです。

この検索の仕組みはFOSS4G Hiroshima 2026の発表で説明したので、詳細は発表をまとめた記事をご確認ください。

ペナルティとは?

改めてペナルティの説明です。 ペナルティは単語(文字)と単語(文字)のくっつきやすさなどに付与されるものです。 良い組み合わせほどペナルティの値を小さく設定します。 そして、ペナルティの小さいものを優先的に使って検索をします。

ペナルティの例:

  • 「北海」と「道」はくっつきやすい
    • ペナルティ0
  • 「北海」と「阿部」はくっつきにくい
    • ペナルティ0.5
  • 「北海道」は住所でよく使われる
    • (住所検索においては)ペナルティ0
  • 「阿部智晃」は住所で使われにくい
    • (住所検索においては)ペナルティ0.5

ペナルティがどういうものかわかったので、いい感じに設定されないとはどのような状態かの説明をしたいのですが、その前にそもそもどのように分割されるのかを確認します。

日本語は1文字区切り

前回の発表の記事内では説明がわかりやすくなるように単語単位で分割されるような説明をしていましたが、漢字(日本語・中国語)の場合、基本的に1文字区切りになります。 形態素解析のようなことは行わず、単純に1文字区切ります。 これはNominatimが漢字を中国語の読みに変換し、その読みをベースに分割しているためです。 (例: 北海道を変換すると「bei hai dao」と1文字ごとに区切られる。)

「北海道大学付属図書館」であれば「北」、「海」、「道」、「大」、「学」、「付」、「属」、「図」、「書」、「館」に分割されます。 意味を無視して単純に分割しているので、先ほどのくっつきやすいうんぬんが関係なくなります。

このような単純な分割が行われた場合、区切りごとに一律でペナルティは0.3になるとします。 (実際の実装では違う値です。あくまで説明用の値です。)

具体的なクエリー文字列でペナルティがいくつになるか計算してみます。

  • 「北海道大学付属図書館」 => 北|海|道|大|学|付|属|図|書|館
    • 区切り(|)ごとにペナルティ0.3なので、0.3 * 9区切り = 2.7 でペナルティは「2.7」
  • 「北海道」 => 北|海|道
    • 区切り(|)ごとにペナルティ0.3なので、0.3 * 2区切り = 0.6 でペナルティは「0.6」

(どちらの例もいい感じにペナルティは計算されてなさそうですが、クエリーが短いとペナルティの影響を受けにくそうなことがわかります。)

仮に単語で分割されていた場合

単語で分割されていたとするとペナルティもいい感じに低く設定されるはずです。

例えば「北海道大学付属図書館」 => 北海道|大学|付属|図書館のように分割されたとします。 このとき「北海道 + 大学」の組み合わせはあり得るからペナルティ0、他の組み合わせも全部あり得るから全部ペナルティ0とします。 ということで、全部くっつきそうなので、0 * 3区切り = 0 でペナルティは「0」という可能性もあります。

これが「いい感じに設定されたペナルティ」です。

ペナルティがいい感じに設定されない

だいぶ単純化して説明しましたが、漢字(日本語・中国語)の場合は単純に1文字区切りになることでペナルティが高く設定されており「ペナルティがいい感じに設定されない」=「ペナルティが無駄に高く設定される」のが、わかっていただけたのではないでしょうか。

なぜ高いと見つからないか

では、ペナルティが高いとなぜ検索でヒットしないのでしょうか。 それはペナルティが高いと検索で使われなくなってしまうからです。

ざっくり流れを説明します。

  1. 「どこまでが名前で、どこからが住所か」の候補をたくさん作る
    • Nominatimはクエリーのどこが名前でどこが住所か事前に知らないので、「名前=北海道大学付属図書館」「名前=大学付属図書館、住所=北海道」「名前=図書館、住所=北海道大学付属」…のように振り分けの候補をたくさん作ります
    • (この候補の作り方の詳細は前回の発表の記事の巻末で説明しています)
  2. その候補の中からペナルティの小さい順に検索する
    • 見つかるまで繰り返す
  3. ペナルティが一番小さい候補 + 1.5 を超えたらそこで終わり

「北海道大学付属図書館」の場合も見つかるはずの候補はあるのですが、上述の無駄に高くなるペナルティのせいでそれが検索に使われず見つからない、という状態になっています。

改善のアイディア

無駄に高くなるペナルティが問題なので、それを改善すればよさそうです。

ペナルティを適切に設定するために日本語の分割を形態素解析などで頑張る案もありますが、それだといろいろ影響範囲が多かったりで大変そうなので、今回は別の手法で検証しました。

日本語で検索する場合、ほとんどのケースでは漢字が連続していたらそれは1つの単語になることが多いはずです。 ということで、日本語が連続するものはペナルティを低めにするアプローチで検証してみます。

説明だけだとわかりにくそうなので例を示します。北|海|道|大|学|付|属|図|書|館を考えます。

  • 改善前: 区切り(|)ごとにペナルティ0.3
    • 0.3 * 9区切り = 2.7
  • 改善後: 区切り(|)の前後が日本語(かな・カナ・漢字)だったらペナルティ0.1
    • くっつきやすいはずなので低めにする
    • 0.1 * 9区切り = 0.9

ペナルティはだいぶ低くなっています。

実装案

追加したコードの一部です。

PENALTY_CJ_CHAR = 0.1
# ...略

def get_cj_partial_penalty(self, trange: TokenRange) -> float:
    penalty = self.nodes[trange.start].partial.penalty
    for pos in range(trange.start + 1, trange.end):
        if self.is_cj_continuation(pos): # 日本語が続いていたら
            penalty += PENALTY_CJ_CHAR # ペナルティは0.1
        else:
            node = self.nodes[pos]
            # そうでなかったら既存のペナルティの追加
            penalty += node.partial.penalty + node.word_break_penalty
    return penalty

変更箇所全部: https://github.com/abetomo/Nominatim/pull/1/changes

デモ: ペナルティ改善なし

デモ環境には北海道 + 中国地方 + 関西地方のデータが登録されています。

$ docker compose exec -e NOMINATIM_CJK_PENALTY=off nominatim \
  nominatim search --query "広島平和記念資料館"
[]
$ docker compose exec -e NOMINATIM_CJK_PENALTY=off nominatim \
  nominatim search --query "北海道大学付属図書館"
[]

見つかりません。

デモ: ペナルティ改善あり

$ docker compose exec -e NOMINATIM_CJK_PENALTY=on nominatim \
  nominatim search --query "広島平和記念資料館" --namedetails
[
    {
        "place_id": 1131164,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "way",
        "osm_id": 60867022,
        "lat": "34.3915495",
        "lon": "132.4530979",
        "category": "tourism",
        "type": "museum",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "tourism",
        "name": "広島平和記念資料館東館",
        "display_name": "広島平和記念資料館東館, 2, 平和大通り, 中島町, 中区, 広島市, 広島県, 730-0811, 日本",
        "namedetails": {
            "name": "広島平和記念資料館東館",
            "name:de": "Friedensmuseum Hiroshima Ostflügel",
            "name:en": "Hiroshima Peace Memorial Museum East Building",
            "name:fr": "Bâtiment Est du Musée du Mémorial de la Paix de Hiroshima",
            "name:ja": "広島平和記念資料館東館",
            "name:ru": "Восточное здание Мемориального музея мира в Хиросиме",
            "name:ja-Hira": "ひろしま へいわ きねん しりょうかん ひがしだて",
            "name:ja-Latn": "Hiroshima-heiwa-kinen-shiryōkan-higashidate"
        },
        "boundingbox": [
            "34.3912687",
            "34.3918269",
            "132.4527648",
            "132.4534301"
        ]
    },
    {
        "place_id": 1138186,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "way",
        "osm_id": 60867023,
        "lat": "34.3918121",
        "lon": "132.4521048",
        "category": "tourism",
        "type": "museum",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "tourism",
        "name": "広島平和記念資料館本館",
        "display_name": "広島平和記念資料館本館, 平和大通り, 中島町, 中区, 広島市, 広島県, 730-0811, 日本",
        "namedetails": {
            "name": "広島平和記念資料館本館",
            "name:de": "Friedensmuseum Hiroshima",
            "name:en": "Hiroshima Peace Memorial Museum Main Building",
            "name:fr": "Bâtiment principal du Musée du Mémorial de la Paix de Hiroshima",
            "name:ja": "広島平和記念資料館本館",
            "name:ru": "Главное здание Мемориального музея мира в Хиросиме",
            "name:ja-Hira": "ひろしま へいわ きねん しりょうかん ほんかん",
            "name:ja-Latn": "Hiroshima-heiwa-kinen-shiryōkan-honkan"
        },
        "boundingbox": [
            "34.3916233",
            "34.3920004",
            "132.4516421",
            "132.4525610"
        ]
    },
    {
        "place_id": 1136533,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "node",
        "osm_id": 6167496979,
        "lat": "34.4034644",
        "lon": "132.4651447",
        "category": "tourism",
        "type": "museum",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "tourism",
        "name": "旧広島逓信病院外来棟平和資料館",
        "display_name": "旧広島逓信病院外来棟平和資料館, 16, 東白島町, 中区, 広島市, 広島県, 730-8798, 日本",
        "namedetails": {
            "name": "旧広島逓信病院外来棟平和資料館",
            "name:en": "Former Outpatient Wing of Hiroshima Teishin Hospital Peace Museum",
            "name:ja": "旧広島逓信病院外来棟平和資料館",
            "official_name": "広島平和記念資料館附属展示施設 旧広島逓信病院外来棟平和資料館"
        },
        "boundingbox": [
            "34.4034144",
            "34.4035144",
            "132.4650947",
            "132.4651947"
        ]
    }
]

補足: 「旧広島逓信病院外来棟平和資料館」も見つかっています。 これはofficial_nameの「広島平和記念資料館附属展示施設」に部分一致しているためです。

$ docker compose exec -e NOMINATIM_CJK_PENALTY=on nominatim \
  nominatim search --query "北海道大学付属図書館"
[
    {
        "place_id": 2858861,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "relation",
        "osm_id": 2618243,
        "lat": "43.0719435",
        "lon": "141.3448167",
        "category": "building",
        "type": "university",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "building",
        "name": "北海道大学付属図書館本館",
        "display_name": "北海道大学付属図書館本館, 新渡戸通り, 北11条西5, 北区, 札幌市, 石狩振興局, 北海道, 北海道地方, 060-0810, 日本",
        "boundingbox": [
            "43.0715688",
            "43.0723911",
            "141.3441960",
            "141.3452698"
        ]
    },
    {
        "place_id": 2589538,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "way",
        "osm_id": 298957650,
        "lat": "42.3785731",
        "lon": "141.0339702",
        "category": "amenity",
        "type": "library",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "amenity",
        "name": "室蘭工業大学附属図書館",
        "display_name": "室蘭工業大学附属図書館, 室蘭環状線, 高砂町五丁目, 室蘭市, 胆振総合振興局, 北海道, 北海道地方, 050-0072, 日本",
        "boundingbox": [
            "42.3783211",
            "42.3787309",
            "141.0336521",
            "141.0343146"
        ]
    },
    {
        "place_id": 2873368,
        "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
        "osm_type": "way",
        "osm_id": 569512769,
        "lat": "43.1906783",
        "lon": "140.9802463",
        "category": "amenity",
        "type": "library",
        "place_rank": 30,
        "importance": 9.99999999995449e-06,
        "addresstype": "amenity",
        "name": "小樽商科大学付属図書館",
        "display_name": "小樽商科大学付属図書館, 地獄坂, 富岡一丁目, 緑, 小樽市, 後志総合振興局, 北海道, 北海道地方, 047-8501, 日本",
        "boundingbox": [
            "43.1904978",
            "43.1908589",
            "140.9798907",
            "140.9806020"
        ]
    }
]

両方ともヒットしました!

副作用

「北海道大学付属図書館」で検索した例だと「室蘭工業大学附属図書館」が見つかるのは違和感があります。 (関連はあるけど求めていないデータも一緒に出てきますが、変更前は0件だったので改善はしているはず! この結果についてご意見をもらえるとうれしいです!)

どうしてこれが見つかるようになったのかは「なぜ高いと見つからないか」で触れた「名前と住所の振り分けの候補」と「日本語は1文字区切り」で触れた「漢字は中国語の読み」で説明できます。

副作用の解説

今回ヒットしたのは「名前 = 大学付属図書館、住所 = 北海道」という分け方の候補で検索したときです。 「北海道大学付属図書館」をまるごと名前として探す候補ではありません。

ペナルティ改善前は、この候補はペナルティの値が高く検索に使われませんでしたが、ペナルティ改善で検索に使われるようになりました。

検索は部分一致で行われるので

  • 名前検索
    • 「クエリー: 大学付属図書館」が「データ: 室蘭工業大学附属図書館」に部分一致
  • 住所検索
    • 「クエリー: 北海道」が「データ: 北海道室蘭市...」に部分一致

になるので納得感のある結果です。

一見納得感のある結果に見えますが、よく見ると納得できないポイントがあります。

  • 「北海道大学付属図書館」
  • 「室蘭工業大学附属図書館」

「ふ」の漢字が違いますね!

これは「漢字は日本語でも中国語の読み」で説明できます。 検索するときもこの中国語の読みで行われていて、「付」と「附」は同じ読みなので、漢字の見た目は違いますが、内部で保持しているデータでは同一なのでマッチした、というわけです。

効果がある範囲

中国語では未検証ですが、日本語と同じ漢字なので、おそらく同じように改善するのではないかと思います。 ただし今回の変更は韓国語には効果はなさそうです。 実は韓国語の場合は1文字ずつ分割されず、状況が違うためです。

(上述の実装例のメソッド名とかがcjだったのはこのため。)

韓国語を分割した例

$ docker compose exec -T nominatim python3 -c "
import icu

t = icu.Transliterator.createInstance('Any-Latin; Latin-ASCII; Lower')
for s in ['북해도', '北海道']:
    print(s, '->', t.transliterate(s))
"
북해도 -> bughaedo
北海道 -> bei hai dao

出力を見ると韓国語の方はスペースがありません。 これは1つの単語として扱われていることを示しています。 逆に日本語の方はスペースで区切られ、1文字ずつ分かれていることが確認できます。

他のアプローチ

先に軽く触れましたが、日本語の単語ごとに正しく区切って、正しくペナルティを設定するのがきれいなやり方だと思います。

ICUのBreakIteratorを使うといい感じに分割できそうなので、道具は揃っていそうです。 が、インポート全体に影響する変更になるので、全言語・全データで検証したりとかが大変そうです。

もっと良いアイディアがあれば教えてください!

ICUのBreakIteratorで分割の例

$ docker compose exec -T nominatim python3 -c "
import icu

text = '北海道大学付属図書館'
bi = icu.BreakIterator.createWordInstance(icu.Locale('root'))
bi.setText(text)
parts = []
start = 0
for end in bi:
    parts.append(text[start:end])
    start = end
print('|'.join(parts))
"
北海道|大学|付属|図書館

単語で区切られているのが確認できます。

まとめ

ペナルティがいい感じに設定されていなかったので、日本語が連続するものはペナルティを低めにして改善するアプローチを検証しました。 そもそも1文字ずつの分割がよくないので、そこから改善できるとより良さそうです。

参考リンク