🗾

国土数値情報の行政区域ポリゴンを地図アプリに組み込む——mapshaperによるGeoJSON軽量化とデータバインディング

に公開

個人開発の地図アプリに都道府県や市区町村の行政区画ポリゴン・行政境界ラインを表示するときに行なった作業のまとめです。

はじめに

Web地図アプリケーションの開発において、標準のマーカークラスター表示の代わりに、市区町村などの行政境界ごとにデータの件数を色塗りで表現する「コロプレスマップ(階級区分図)」、人口動態や世帯数などの統計データをマッピングして店舗の出店計画や需要予測に活かす 「商圏分析(エリアマーケティング)」ダッシュボード など、行政区画データを地図に組み込みたいケースは多くあります。

しかし、国土交通省の「国土数値情報」から全国の行政区域データをそのままダウンロードすると、GeoJSONのファイルサイズは500MB以上に達することがあり、そのままブラウザで読み込ませることは現実的ではありません。また、単純な Simplify(間引き・簡素化)処理を限界まで適用してもファイルサイズが20MB以下に下がりにくく、フロントエンドでの初期ロードや描画パフォーマンスを考慮すると、依然として実用的なサイズを超えてしまいます。

この記事では、国土交通省の行政区域データを使い、地理的形状の品質を実用レベルに保ったまま、ファイルサイズを数MBまで激減させる最適化手順と、軽量なRDB(SQLite等)のデータと動的に紐付けるフロントエンドのアーキテクチャ設計について解説します。


1. 国土数値情報からのデータ選定

データの一次ソースには、国土交通省が提供する「国土数値情報ダウンロードサイト」のデータを使用します。

今回は、全国の「世界測地系(Shapefile)」データをダウンロードします。データ加工のプロセスでは、ファイルサイズが大きくテキスト解析のオーバーヘッドがある .geojson ではなく、バイナリ形式で軽量にハンドリングできる Shapefile(.shp, .shx, .dbf, .prj の4点セット) をまず使用します。


2. mapshaperを用いた軽量化プロセス

地理空間データの編集には、オープンソースのコマンドライン・Webベースツールである mapshaper.org を利用します。

簡素化(Simplify)だけではサイズが下がらない理由

全国の行政区域データには、日本全国の無数の「島嶼(とうしょ)部」や「飛び地」がすべて独立したオブジェクト(ポリゴン)として含まれており、その数は10万個を超えます。
たとえば、離島を多く持つ自治体では、同じ行政区域コードを持つポリゴンが数百個に分割されて存在します。そのため、どれだけポリゴンの輪郭線を滑らか(Simplify)にしても、フィーチャー数そのものが多いことによるJSONの記述量がボトルネックとなり、ファイルサイズが小さくなりません。

解決策:行政区域コードによる融合(dissolve

この問題は、同じ市区町村コードを持つバラバラの島や区画を1つの「マルチポリゴン(MultiPolygon)」に結合する dissolve コマンドで解決できます。これにより、オブジェクト数を全国の市区町村数・行政区数(約1,700個)まで一気に削減できます。

また、mapshaper は単純な点の間引きだけでなく、隣接するポリゴン間のトポロジー(接続関係)を維持したまま簡素化を行うため、境界線に不自然な隙間や重なりができるのを防ぐことができます。

具体的な加工手順

  1. ファイルのインポート:
    mapshaper.org を開き、Shapefileの 4点セット(.shp, .shx, .dbf, .prj を同時にブラウザ画面にドラッグ&ドロップして import します。
  2. ポリゴンの融合とIDの数値化(コマンド実行):
    画面上部の Console ボタンを押し、下部の入力欄に以下のコマンドを入力して実行します。
# 行政区域コード(N03_007)で融合し、必要な属性をコピー
dissolve N03_007 copy-fields=N03_001,N03_002,N03_003,N03_004

# MapLibreの型制約に対応するため、文字列のコードを数値のidに変換して付与
each "id=parseInt(N03_007)"

N03_007 は5桁の行政区域コード(JISコード)です。MapLibreの仕様に適合させるため、同時に数値型の id フィールドを生成しておきます(詳細は後述)。

  1. 境界線の簡素化(Simplify):
    右上の SimplifyApply をクリック。スライダーを左にドラッグし、 0.1%0.2% 程度まで境界線を間引きます。
    ※全国を俯瞰するレイヤであれば、この程度の削減でも視覚的な差異が出にくい傾向があります。ただし、沿岸部や島嶼が多い地域では海岸線の省略が目立つ場合があるため、実際の出力を確認しながら調整してください。詳細な拡大表示が必要な場合は、「ズームイン時に個別マーカーや高精度レイヤに切り替える」といったUI設計を組み合わせることで、パフォーマンスと表現力を両立できます。
  2. エクスポート:
    右上の ExportGeoJSON を選択してダウンロードします。

この最適化により、必要な属性情報(都道府県名や市区町村名)を維持したまま、ファイルサイズを 数MB程度まで削減した GeoJSONファイルを作成できます(元データが500MB超の場合、この手順で概ね1〜5MB台に収まることが多いです)。


3. アプリケーションにおける動的データ結合のアーキテクチャ設計

軽量化したGeoJSONに対して、データベースに格納されているリアルタイムな件数データを紐付ける場合、「GeoJSONファイル自体に件数データを書き込まない(ジオメトリと属性データを完全に分離する)」 設計が推奨されます。

役割の分離

  • 静的ジオメトリ(アセット): 変化しない「市区町村の枠線データ(GeoJSON)」。プロジェクトの静的配置ディレクトリ(static/ など)に置き、CDN経由でキャッシュ配信します。各ポリゴンには5桁の行政区域コード(N03_007)および数値の id を持たせておきます。
  • 動的データ(データベース): 変動する「市区町村ごとの集計件数データ」。バックエンドのデータベース(SQLiteやPostgreSQLなど)側で GROUP BY を用いて集計し、必要なコードと件数のみを軽量なJSON API(数KB)として返します。

フロントエンド(MapLibre GL JS)側でのデータバインディング

ブラウザ側でGeoJSONと集計APIの双方をフェッチし、MapLibreの feature-state 機能を使用して、メモリ上で動的に結合して描画(色塗り)を行います。

// 1. マップの初期化と静的GeoJSONソースの追加
map.on('load', async () => {
    map.addSource('national-cities', {
        type: 'geojson',
        data: '/data/national_cities.geojson' // 事前に mapshaper で id フィールドを付与したもの
    });

    map.addLayer({
        id: 'city-fills',
        type: 'fill',
        source: 'national-cities',
        paint: {
            // feature-state の 'count' 値に応じて色を動的に変化させる
            'fill-color': [
                'case',
                ['!=', ['feature-state', 'count'], null],
                [
                    'interpolate', ['linear'], ['feature-state', 'count'],
                    0, '#f7f7f7',
                    10, '#ffe3e3',
                    100, '#ff6b6b'
                ],
                '#cccccc' // データが取れなかった場合のデフォルト色
            ],
            'fill-opacity': 0.6
        }
    });

    // 2. データベースの集計APIから最新データを取得して動的にバインド
    try {
        const response = await fetch('/api/city-counts');
        const countsFromDB = await response.json();
        // 例: [{ city_code: "13101", count: 42 }, ...]
        // city_code は文字列で返ってくる場合が多いため、Number() で数値に変換する

        countsFromDB.forEach(row => {
            map.setFeatureState(
                { source: 'national-cities', id: Number(row.city_code) },
                { count: row.count } // 動的に状態(状態値)を注入
            );
        });
    } catch (error) {
        console.error('データの取得またはバインディングに失敗しました:', error);
    }
});

注意点

1. feature-stateにおけるIDのデータ型

MapLibre GL JS(およびMapbox GL JS)の setFeatureState() は、対象フィーチャーの id として整数、またはintegerにキャストできる文字列を渡す必要があります。行政区域コード(N03_007)は先頭のゼロ(例:北海道の 01100)を維持するために文字列として扱われがちですが、"01100" のような文字列IDをそのまま使うと、parseInt によって 1100 として解釈されます。このズレにより、DB側の結合キーと一致しなくなる場合があります。
そのため、前述の mapshaper の手順において each "id=parseInt(N03_007)" を実行し、GeoJSONのトップレベルに数値型の id フィールドを明示的に埋め込んでおくアプローチが確実です。この時点で先頭のゼロは失われるため、DB側の結合キーも文字列の行政区域コードではなく、同じく数値型で管理する必要があります。また、APIレスポンス側でも city_code が文字列で返ってくる場合があるため、上記コードのように Number() で変換してから渡すようにしてください。

2. 非同期処理のタイミング管理

上記のサンプルコードでは、map.on('load') イベント内でソースの追加からAPIのフェッチ、setFeatureState の適用までを一連の流れとして実行しています。ソースのロードが完了する(マップがデータを認識する)前に setFeatureState を呼び出すと、状態が反映されないかエラーになる可能性があるため、必ずマップのライフサイクルイベントの内部、または適切な非同期ハンドリングを行ってください。


補足:出力データのプロパティ構造

dissolve コマンドで copy-fields を指定した場合、以下の国交省定義の属性がマルチポリゴンに引き継がれます。

  • N03_001: 都道府県名
  • N03_002: 支庁・振興局名
  • N03_003: 郡・政令指定都市名
  • N03_004: 市区町村・政令市の区名
  • N03_007: 行政区域コード(5桁のJISコード)
  • id: (mapshaperで追加した数値型の一意なID)

まとめ

国土交通省の一次ソースデータからトポロジーを維持して簡素化する手順を持っておくことで、データの基準年が変わった際や、町丁目レベルへの応用にも対応できるでしょう。
巨大な地理空間データに起因するボトルネックを解消し、軽量な静的アセットとDB集計を組み合わせることで、クライアントサイド主導で軽快に動作するWebGISアプリケーションを構築できます。


関連リンク


Discussion