1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本全国約500館を網羅!React × Leaflet × Firebase で作る「高精度」水族館チェックリスト開発記

1
Last updated at Posted at 2026-03-02

はじめに

日本全国の水族館巡りを趣味とする方に向けて、約500の施設(水族館、動物園、ふれあい施設等)を網羅した「全国水族館チェックリスト&マップ」を開発しました。

完成したサイトはこちら:
全国水族館チェックリスト&マップ

本記事では、特に 「国内における高精度なジオコーディングの実現」「ログイン不要な状態を維持しつつ、シームレスに複数端末同期を行う実装」 について技術的な深掘りを行います。


1. データの統合とクレンジング

元データは「既存サイトのJavaScript配列」と「最新のCSVリスト」の2系統に分かれていました。

直面した課題

  • 同一施設でも名称が微妙に異なる(例:「美ら海水族館」と「沖縄美ら海水族館」)。
  • 住所に建物名や「(旧〜小学校)」などの補足情報が含まれ、座標変換APIのヒット率を下げている。

解決策:Pythonによる名寄せスクリプト

正規表現を用いて住所から郵便番号や補足情報を除去し、施設名をキーとしたマージを行いました。また、廃止された施設をフラグで除外するフィルタリングもこの段階で実施しました。


2. ジオコーディング戦略の変遷:なぜ一度「失敗」したのか

開発の初期段階では、緯度経度を取得するために世界的に広く使われている Nominatim (OpenStreetMap) を使用していました。しかし、実際にマップを構築してみると、大きな問題に直面しました。

Nominatim 採用時の失敗

  • 「海上ピン」の発生: 海沿いの施設(水族館に多い)の住所を広域で捉えてしまい、計算された代表点が海の上にプロットされる事態が多発しました。
  • 市区町村の中央への密集: 詳細な番地解析に失敗すると、とりあえず「市役所」や「市の中央」の座標を返すため、同じ市内の複数の施設が1点に重なってしまいました。
  • レート制限の壁: 1秒に1リクエストという厳しい制限があり、500件のデータを処理するのに時間がかかりすぎる上、途中で接続が遮断される不安定さがありました。

国土地理院API (GSI API) への転換

「日本の住所を完璧に当てるには、日本の公的データを使うのが最短ルートである」という結論に至り、 国土地理院の住所検索API へ切り替えました。

これにより、以下の改善が得られました。

  • 番地・号レベルの精度: 日本国内の「丁目・番地・号」を極めて正確に解釈。
  • 陸地保証: 座標が海上に飛ぶことがなくなり、正確に敷地内にピンが刺さるようになりました。
  • 安定性: 国内向けのAPIであるため解析精度が非常に高く、フォールバック(町名レベルへの格下げ検索)の制御も容易でした。

3. ジオコーディングの深掘り:海上ピンとの戦い

初期の実装では OpenStreetMap 系の Nominatim API を使用していましたが、 「施設が海沿いにあるため、広域な地番を解析した結果、代表点が海上になってしまう」 という致命的な問題が発生しました。

国土地理院API (GSI API) への転換

日本の住所に特化した 国土地理院の住所検索API を採用することで解決しました。

import urllib.parse
import urllib.request

def get_exact_coords(address):
    # 建物名などを除去して、地番・号レベルまでにする
    clean_addr = extract_street_address(address)
    url = f"https://msearch.gsi.go.jp/address-search/AddressSearch?q={urllib.parse.quote(clean_addr)}"
    
    # GSI APIは [longitude, latitude] の順で返す
    # 地番までヒットすれば、建物の敷地内に正確にプロット可能

多重フォールバック戦略

  1. 絶対座標キャッシュ: 有名館(アドベンチャーワールド等)は手動で緯度経度を固定。
  2. 地番レベル検索: GSI APIで詳細な位置を特定。
  3. 町名レベル検索: 詳細住所でヒットしない場合、番地を削って再試行。
  4. 役所プロット: どうしても特定できない場合は、その住所にある「役所」の位置を取得。

これにより、 「海上ピンを撲滅し、100%陸地に表示させる」 という信頼性を担保しました。


3. 地図描画のパフォーマンスとUX

約500件のピンを一度に React で描画すると、特にモバイル端末でスクロールがカクつく問題がありました。

Marker Clustering

react-leaflet-cluster を導入。ズームレベルに応じてピンを束ねることで、描画負荷を劇的に軽減しました。

モバイル特化のレイアウト(ボトムシート)

PC版では右側に詳細パネルを表示しますが、スマホ版では画面下からせり上がるボトムシート形式に動的に切り替えます。

const isMobile = window.innerWidth < 768;

// 条件分岐によるレイアウト変更
<div style={{
  position: 'absolute',
  bottom: 0,
  height: isMobile ? '85vh' : '100%',
  width: isMobile ? '100%' : '400px',
  borderRadius: isMobile ? '20px 20px 0 0' : '0',
  // ...
}}>

4. サーバーレス・同期アーキテクチャ

「ログインしなくても使える」という手軽さを維持しつつ、Firebaseを活用してデータ同期を実現しました。

データのライフサイクル

  1. 未ログイン: localStorage に訪問済みIDを保存。
  2. ログイン時: Firebase Auth (Google) でUID取得。Firestore の users/{uid} ドキュメントを読み込み。
  3. マージ処理: ログインした瞬間に、localStorage にあった未保存データとクラウド上のデータを統合して Firestore へ書き戻し。

Firestore のセキュリティルール

自分自身のデータのみを操作できるよう、強固なセキュリティルールを適用しています。

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

5. SEOと発見可能性の向上

単なる「マップ」ではなく「チェックリスト」としての需要を掘り起こすため、以下の対策を行いました。

  • Titleタグの最適化: 「全国水族館チェックリスト」を先頭に配置。
  • フィルタリング機能: 「未訪問のみ表示」フィルタを実装。これにより、ユーザーが次にどこへ行くべきかを一瞬で判断できるツールとしての価値を高めました。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?