Zenn
🔣

地理院タイルに使われている文字を調べてみた

に公開

きっかけ

MapLibre Meetup Japan #04 で MapLibre GL JS のフォントに関する発表があり、ぜんぜん仕組みを知らなかったのでおもしろいなと思いました。

https://mug-jp.connpass.com/event/345527/

とりあえず実際のデータを見てみよう、ということで、glyphs の .pbf をパースして覗くツールを作って、maplibre/demotiles のファイルをいくつか眺めてみました。

https://github.com/yutannihilation/dump-glyphs-pbf

で、眺めてみて思ったのは、「こんな文字、日本の地名で使われてるの...?」という文字がけっこうあることでした(今思えば、Noto Sans なので日本以外の文字も入っていて当然ですね)。もしこれを、実際に MapLibre に表示する必要がある文字だけに絞り込めればもっと軽くなるのでは...? ということで、調べることにしました。

元データを探す

「実際に MapLibre に表示する必要がある文字」と書きましたが、つまり、地名とか施設名とか、MapLibre GL JS でラベルとして表示されている文字が知りたいことです。調べる対象は、とりあえず国土地理院ベクトルタイルに絞ります。

地理院タイルダウンロードツールを使ってタイルをダウンロードしないといけないのかな、でもこんな趣味的な目的のためにタイルサーバーに負荷をかけるのも申し訳ないし...、と迷っていたところ、PMTiles ファイルが用意されていることを知りました。タイル丸ごとではなくてラベルだけのデータがどこかにあればいいのですが、なさそうだったのでこれを使ってみることにします。

https://github.com/gsi-cyberjapan/optimal_bvmap

PMTiles のパーサを書く

みなさんは、料理をするとき、包丁をつくるところからはじめるタイプでしょうか。私は違いますし、もし知り合いがそういうことをやろうとしていたら、やめておけ、死ぬぞ、と止めるところですが、まあ PMTiles のパーサは書いても死なないので、勉強のために書いてみることにしました[1]。Rust で。

仕様

PMTiles の仕様は公式を読むか、この日本語の解説を読むといいと思います。私は、全部終わった後でこの解説の存在に気付きました。

https://qiita.com/mg_kudo/items/3179639c79c1734fd373

要点としては、

  • header、root directory、metadata、leaf directory、tile data という5つのセクションに分かれている
  • それぞれのセクションの区切りはまず header を読めばわかる
  • 実際のタイルは、root directory・leaf directory を読むと、tile data section のどこからどこまでがどのタイルかわかる

という感じです。

nom crate

せっかくなので、割と賛否両論な印象がある nom crate を使ってみることにしました。
バイナリのためのパーサコンビネータ、みたいなやつです。

https://docs.rs/nom/latest/nom/

書き方のスタイルが、パーサコンビネータっぽいのとそうでないのと2種類あって、私はそうでない方で書きました。たとえば、header のパースはこんな感じになります。どうでしょうか...?

https://github.com/yutannihilation/dump-pmtiles-labels/blob/6bb049f38bcdb40a5f99fc49ea2627c41db8a0f3/src/header.rs#L64-L114

使ってみた印象としては、PMTiles のフォーマット程度の複雑さではあまり恩恵がないかもしれません。↑のような書き方をするなら、次やるなら素の Rust で書くかなあ...、という感想でした。もっと複雑なバイナリフォーマットを扱う時があれば、また試してみたいと思います。

variable-length integer

PMTiles の中で使われる integer の値は、variable-length integer というフォーマットで表されています。これは、integer をより少ないバイト数で表現するためのもので、Protocol Buffers 発祥らしいです。

https://protobuf.dev/programming-guides/encoding/#varints

1つの数字は、1~10個のバイト列で表されます。1バイトのうち、

  • 上位1ビット: continuation bit という、次の1バイトに続くかどうかを示すフラグ
  • 残り: 実際のデータ

という形式になっています。つまり、上位1ビットにフラグが立っていないバイトまで読み進め、そこまでのバイト列を使って計算する、という流れになります。

// continuation bit
// v
   0 0 0 0 0 0 0 0
//   ^^^^^^^^^^^^^
//       data

どういう計算かというと、上位1ビットを取った残り7ビットを順番に並べていくだけです。上の仕様に書かれているのを引用すると、こんな感じです。

10010110 00000001        // 
 0010110  0000001        // 最上位ビットを取る
 0000001  0010110        // 逆順に並べ替える
   00000010010110        // くっつける
 128 + 16 + 4 + 2 = 150  // unsigned 64bit integer として解釈

実装は、nom を使って書くとこんな感じです。逆順にして7ビットずつシフトさせて足していっています。

https://github.com/yutannihilation/dump-pmtiles-labels/blob/6bb049f38bcdb40a5f99fc49ea2627c41db8a0f3/src/varint.rs#L7-L20

prost crate

最後、タイルを読むところは prost を使いました。tokio によって開発されている crate です。Protocol Buffers の定義(.proto ファイル)を渡すと Rust の struct を生成して、そのプロトコルのデータをデコードできるようになります。

https://docs.rs/prost/latest/prost/

こんな感じのコードになるんですが、属性値はこの Layervalues に入ってくるので、今回はこのうち string_value に入っている値をすべて抜き出します。(ほんとは、どの keys に対応するものかチェックした方がいいのかもですが、今回はとりあえずざっくり知りたいだけ)。

pub struct Tile {
    pub layers: ::prost::alloc::vec::Vec<tile::Layer>,
}

pub struct Value {
    /// Exactly one of these values must be present in a valid message
    pub string_value: ::core::option::Option<::prost::alloc::string::String>,
    pub float_value: ::core::option::Option<f32>,
    pub double_value: ::core::option::Option<f64>,
    pub int_value: ::core::option::Option<i64>,
    pub uint_value: ::core::option::Option<u64>,
    pub sint_value: ::core::option::Option<i64>,
    pub bool_value: ::core::option::Option<bool>,
}

pub struct Layer {
    ...
    pub keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
    pub values: ::prost::alloc::vec::Vec<Value>,
}

(ちなみに、この Value は、Protocol Buffers 的には optional なフィールドを持つことはコストがかからないと思いますが、Rust 的にはムダに幅を取ることになるので、うーん...という気持ちになりました。何かいい方法あるんでしょうか?)

結果

さて、これが結果です。数分で抽出できました。

数字が上位なのは「まあそういうのもかな...?」と思うかもですが、「m」が一位なのには少し面食らうかもしれません。これは、「5.5m-13m未満」「3m-5.5m未満」といった文字列から来ているようです。これはどうやら、道路中心線の属性らしいです。地理院地図 Vector を軽く見まわした感じ、表示されるラベルには使われてなさそうでした。

メインの結果

まあ、そういう余計な文字が入っているのもありつつ、知りたかった文字数はというと...

3892文字

でした。どうでしょうか? 多いと感じますか? 参考になりそうな数字をいくつか挙げてみましょう。

  • 郵便番号のデータを調べたという記事では、3961文字となっています。やや少ないのが気がかりですが、だいたいのサイズ感はあってそうです。(追記:ちゃんと読むと、これは「延べ文字数」でした。なのでまあ、多くて当然ですね)

  • 「Noto Sans サブセット化」とかで検索すると、JIS第一水準とひらがなカタカナくらいに絞り込む方法の記事がひっかかりますが、JIS第一水準の漢字は2,965字です。

  • Noto Sans CJK JP は、Adobe font のページによると、「JISX0208 日本でごく一般的に使用されている文字を指定します。これには、6,355 個の漢字と数百個のかな、句読点、その他の記号が含まれます。 」となっています。

最後の事実を踏まえると、Noto Sans を今回得た文字のみに絞り込んでも、ふつうに日本語だけに絞り込んだ時の半分程度にしか減らなそうです。なので、これをすることで劇的にパフォーマンスがよくなることはなさそうです。

「♡」

さて、メインの結果はそれくらいにしてどうでもいい話をすると、集計が正しければ、「♡」が4回使われているらしいです。これ以外に記号(「☆」とか「△」とか)っぽい文字はないので、いったい♡がどこで使われているのか気になります...

追記:
識者の方に教えてもらいましたが、岡山にこういう橋があるらしいです。↑では4回と書いたんですけど、大きな構造物はおそらくズームレベル別に重複してカウントしているので、ここが唯一の♡なんだと思います。

https://www.okayama-kanko.jp/spot/11085

感想

「こんな文字、日本の地名で使われてるの...?」という疑問からはじまったこの試みでしたが、けっきょく出てきた集計結果を見ても「こんな文字、日本の地名で使われてるの...?」という感想になりました。何の成果も得られませんでしたが、フォントの難しさと日本の広さを再確認できてよかったです。

脚注
  1. あと、公式の JavaScript や C++ の実装、非公式の Rust 実装とかもあるんですが、そもそも PMTiles は配信のためのものなので、中身のタイルの属性値だけ舐めるみたいな機能はなさそう?と思ったのもあります。あんまりちゃんと調べてないので、勘違いかもです。 ↩︎

MIERUNEのZennブログ

Discussion

ログインするとコメントできます