見出し画像

Leaflet+国土地理院API+郵便番号検索APIで郵便番号or住所検索で地図表示

今回の完成イメージ(Plunkerのプレビュー画面に飛びます)
※警告画面が表示されますが、問題なければ「Proceed」をクリックしてください。気になる方は「Back」で戻るかタブを閉じてください。

ざっくり経緯

  • これまでGoogleMapsAPIを使用して、店舗一覧及び郵便番号・住所からの店舗検索機能を実装していた

  • しかし、アクセス数が増えるとともにGoogleMapsAPIの使用料も増えてきた

  • 想定外の出費となりつつあるので、同様の機能でお金がかからない仕組みを導入したい

ということで、今回は以下のライブラリ・APIを用いて解消したいと思う。

  • 地図表示にはLeaflet(商用利用可)を使用

  • 住所検索には国土地理院地図のAPI(商用利用可)を使用

  • 国土地理院APIでは郵便番号から緯度・軽度を出せないので、郵便番号検索API(商用利用可)を使用して一旦住所を出す

国土地理院API及び郵便番号検索APIは、いずれも利用料金は基本的にかからない。

ただし、以下規約・制約等があるので使用上注意は必要。

今回の対応内容

①HTMLの作成

LeafletはCDNを使用。CSS、JSファイルは公式推奨通りheadタグ内に設置。

<!doctype html>

<html>
  <head>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
     integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
     crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
     integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
     crossorigin=""></script>
    <link rel="stylesheet" href="lib/style.css">
    <script src="lib/script.js"></script>
  </head>

  <body>
    <div class="wrapper">
      <div id="map"></div>
      <form id="map-search" class="form">
        <input type="text" id="map-search-address" class="form-address" placeholder="郵便番号または住所を入力">
        <button id="map-search-submit" class="form-submit" type="submit">検索</button>
      </form>
    </div>
  </body>
</html>

②CSS(Scss)の作成

Leafletで作成したマップの上にフォームを配置。

body {
  margin: 0;
  padding: 0;
}

.wrapper {
  position: relative;
}

#map {
  position: relative;
  height: 100vh;
  z-index: 1;
}

.form {
  display: flex;
  align-items: center;
  gap: 2px;
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 5;
  cursor: pointer;

  &-address {
    appearance: none;
    height: 25px;
    padding: 0 5px;
    border-radius: 4px;
    border: 1px solid #333;
  }
}

③JSの作成

1. マップ要素を取得し、デフォルト地点を表示

処理は分けてあるが、緯度経度やオプション設定以外はLeafletのチュートリアル通りに作成。

const APP = window.APP = window.APP || {};

APP.map = null;

// 地図表示
APP.SetMap = ($mapElm, lat = null, lng = null) => {
  
  if (APP.map) {
    APP.map.remove();
  }

  APP.map = L.map($mapElm, {
    minZoom: 5,
  }).setView([51.505, -0.09], 13);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
  }).addTo(APP.map);
  APP.map.setView([lat, lng], 16);
};

window.onload = () => {
	// #map要素が存在する場合、地図表示
  const $mapElm = document.querySelector('#map');
  if ($mapElm) {
    // デフォルト位置設定(とりあえず東京近辺に)
    const pos = {
      lat: 35.6684103,
      lng: 139.5760605
    };

		// 地図表示
		APP.SetMap($mapElm, pos.lat, pos.lng);
  }
};

ここで気になるのが以下の条件分岐。

  if (APP.map) {
    APP.map.remove();
  }

これは、検索処理を入れた際に、マップ要素の初期化処理重複エラー(Uncaught (in promise) Error: Map container is already initialized.)を回避するために入れている。

2. 住所入力で検索結果を表示

国土地理院APIを使用し、フォームに入力された住所から緯度経度を算出し、1.で作成したAPP.SetMap処理に渡して結果を表示させる処理を作成。

// 国土地理院APIで緯度経度算出
APP.SearchShopsFromAddress = async ($mapElm, address) => {
  const apiUrl = `https://msearch.gsi.go.jp/address-search/AddressSearch?q=${encodeURIComponent(address)}`;

  const response = await fetch(apiUrl);
  if (!response.ok) {
    alert('住所を取得できませんでした');
    return;
  }

  const data = await response.json();
  if (!data || data.length === 0) {
    alert('住所を取得できませんでした');
    return;
  }

  const lat = data[0].geometry.coordinates[1];
  const lng = data[0].geometry.coordinates[0];
  APP.SetMap($mapElm, lat, lng);
};

window.onloadの処理内に、フォームに入力された住所を(スペースを削除した状態にして)APP.SearchShopsFromAddressに送る処理を作成。

window.onload = () => {
  // #map要素が存在する場合、地図表示
  const $mapElm = document.querySelector('#map');
  if ($mapElm) {
  
		:

    // 住所検索
    const $mapSearchElm = document.querySelector('#map-search');
    if ($mapSearchElm) {
      $mapSearchElm.addEventListener('submit', (e) => {
        e.preventDefault();
        // 入力文字から半角スペースを削除
        const addressInput = document.querySelector('#map-search-address').value.replace(/\s+/g, '');
        // 国土地理院APIで緯度経度算出
        APP.SearchShopsFromAddress($mapElm, addressInput);
		  });
    }
  }
};

3. 郵便番号入力で検索結果を表示

フォームに入力された内容に郵便番号が入っていた場合、郵便番号検索APIで住所検索を行い、その結果を国土地理院APIに渡す処理を作成。(郵便番号が入っている場合は郵便番号による検索処理を優先)

// 郵便番号検索APIで住所検索
APP.SetCodeToAddress = async ($mapElm, postalCode) => {
  console.log($mapElm, postalCode, '郵便番号変換');

  const apiUrl = `https://zipcloud.ibsnet.co.jp/api/search?zipcode=${postalCode}`;

  const response = await fetch(apiUrl);
  if (!response.ok) {
    alert('郵便番号から住所を取得できませんでした');
    return;
  }

  const data = await response.json();
  if (!data || data.length === 0 || data.results === null) {
    alert('郵便番号から住所を取得できませんでした');
    return;
  }
  const addressData = data.results[0];
  APP.SearchShopsFromAddress($mapElm, `${addressData.address1}${addressData.address2}${addressData.address3}`);
};

2.で作成した住所検索時の処理に、入力された内容に郵便番号がある場合の条件分岐を作成。

郵便番号については以下の点にも留意

  • 「〒」の文字が入っている → 「〒」を削除

  • 郵便番号と住所が併記されている → 郵便番号形式の文字列判定を優先

window.onload = () => {
  // #map要素が存在する場合、地図表示
  const $mapElm = document.querySelector('#map');
  if ($mapElm) {
  
		:

    // 住所検索
    const $mapSearchElm = document.querySelector('#map-search');
    if ($mapSearchElm) {
      $mapSearchElm.addEventListener('submit', (e) => {
        e.preventDefault();
        // 入力文字から「〒」文字、半角スペースを削除
        const addressInput = document.querySelector('#map-search-address').value.replace(/\s+/g, '').replace('〒', '');
        // 郵便番号形式の文字列があればaddressに代入
        const address = addressInput.match(/\d{7}|\d{3}-\d{4}/);
        if (address) {
          // 郵便番号検索APIで住所検索
          APP.SetCodeToAddress($mapElm, address[0]);
        } else {
          // 国土地理院APIで緯度経度算出
          APP.SearchShopsFromAddress($mapElm, addressInput);
        }
      });
    }
  }
};

まとめ

以上でLeaflet+国土地理院API+郵便番号検索APIによる郵便番号or住所検索で地図表示の実装は完了となる。郵便番号の扱いについては悩んだが、結果意外とシンプルな振分けでやりたいことを実現することができたと思う。

なお、今回作成したコードは下記ページの通り。


この記事が気に入ったらサポートをしてみませんか?