0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者向け】ZENRIN Maps API を利用した施設検索の実装方法

Last updated at Posted at 2025-12-09

はじめに

地図アプリを使うときに「近くのコンビニを探したい」「周辺の観光スポットを知りたい」と思うことはありませんか?
この記事では ZENRIN Maps API の施設検索機能 を使って、地図上に検索結果を表示する方法を紹介します。
サンプルコードをベースに、マーカーや吹き出し(ポップアップ)を出したり、検索を繰り返せるようにしたりと、実用的な機能をステップごとに解説しています。

地図アプリや業務システムに「場所を検索して表示する機能」を組み込みたい方の入門編として参考になれば幸いです。

この記事でできること

  • ZENRIN Maps API を使って地図を表示
  • 入力した文字列を使って周辺施設を検索
  • 「コンビニ」「病院」などのジャンルボタンで検索
  • API から施設情報を取得(name / address / position / distance 等)
  • 検索結果の各施設をマーカーで地図上に表示
  • マーカーをクリックすると施設名・住所などの情報を吹き出し表示
  • 地図横(または指定エリア)に施設のリストを表示
  • リストで選んだ施設を地図の中心へ移動 & ポップアップ表示
  • 過去の検索結果のマーカー/吹き出しをクリアして再検索

APIキーの取得手順

ZENRIN Maps API を利用するには、事前に APIキーの取得が必要です。

現在、ZENRIN Maps API は 2か月間の無料トライアルが用意されており、期間中は主要な機能を実際にお試しいただけます。開発や評価の初期段階でも安心してご利用いただけます。

APIキーの取得方法については、以下の記事で詳しく解説されています。
初めての方は、まずこちらをご覧いただき、APIキーの発行と設定を行ってください。

ZENRIN Maps APIの始め方

公式リファレンス

ファイル構成

project/
├─ zma_poisearch.html      
├─ css/
  └─ zma_poisearch.css
└─ js/
   └─ zma_poisearch.js

サンプルコード

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ZENRIN Maps API - 施設検索サンプル</title>
  <link rel="stylesheet" href="css/zma_poisearch.css">
  <!-- ZENRIN Maps API ローダー -->
  <script src="https://test-js.zmaps-api.com/zma_loader.js?key=YOUR_API_KEY&auth=referer"></script>
</head>
<body>
  <!-- 検索UI -->
  <div id="search-container">
    <h3>🔍 施設検索</h3>
    <div class="search-row">
      <input type="text" id="searchInput" placeholder="施設名を入力 (例: バスターミナル)">
      <button onclick="searchPOI()">検索</button>
    </div>
    <div class="quick-buttons">
      <button class="quick-btn" onclick="quickSearch('コンビニ', '00140:0014000180')">コンビニ</button>
      <button class="quick-btn" onclick="quickSearch('病院', '00160:0016000110')">病院</button>
      <button class="quick-btn" onclick="quickSearch('ドラッグストア', '00140:0014000230')">薬局</button>
      <button class="quick-btn" onclick="quickSearch('スーパー', '00140:0014000170')">スーパー</button>
      <button class="quick-btn" onclick="quickSearch('ガソリンスタンド', '00110:0011000130')">GS</button>
      <button class="quick-btn" onclick="quickSearch('郵便局', '00170:0017000130')">郵便局</button>
    </div>
  </div>

  <!-- 検索結果表示エリア -->
  <div id="result-container"></div>

  <!-- 地図表示エリア -->
  <div id="ZMap"></div>

  <script src="js/zma_poisearch.js"></script>
</body>
</html>
CSS(クリックで展開)
zma_poisearch.css
body { 
  margin: 0; 
  padding: 0; 
  font-family: Arial, sans-serif; 
}

/* 検索バー */
#search-container {
  position: absolute;
  top: 10px;
  left: 10px;
  background-color: #fff;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  z-index: 1000;
  max-width: 350px;
}

#search-container h3 {
  margin: 0 0 10px 0;
  font-size: 16px;
  color: #333;
}

.search-row {
  display: flex;
  gap: 5px;
  margin-bottom: 10px;
}

#searchInput {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

button {
  padding: 8px 15px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

button:hover {
  background-color: #0056b3;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

/* クイック検索ボタン */
.quick-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.quick-btn {
  padding: 5px 10px;
  background-color: #28a745;
  font-size: 12px;
}

.quick-btn:hover {
  background-color: #218838;
}

/* 結果表示エリア */
#result-container {
  position: absolute;
  top: 160px;
  left: 10px;
  background-color: rgba(255, 255, 255, 0.95);
  padding: 10px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  z-index: 1000;
  max-width: 350px;
  max-height: 400px;
  overflow-y: auto;
  display: none;
}

.result-item {
  padding: 8px;
  margin-bottom: 5px;
  border-bottom: 1px solid #eee;
  cursor: pointer;
  transition: background-color 0.2s;
}

.result-item:hover {
  background-color: #f0f0f0;
}

.result-name {
  font-weight: bold;
  color: #333;
  margin-bottom: 3px;
}

.result-address {
  font-size: 12px;
  color: #666;
}

.result-distance {
  font-size: 11px;
  color: #007bff;
  margin-top: 3px;
  font-weight: bold;
}

.result-count {
  font-size: 12px;
  color: #28a745;
  margin-bottom: 10px;
  font-weight: bold;
}

/* 地図エリア */
#ZMap {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}
zma_poisearch.js
// ======================================
// グローバル変数
// ======================================
let map; // 地図オブジェクト
let markers = []; // マーカー配列
let currentResults = []; // 検索結果
let currentPopup = null; // Popup

// API設定
const API_KEY = 'YOUR_API_KEY';
const POI_SEARCH_URL = 'https://test-web.zmaps-api.com/search/poi';

// ======================================
// 地図初期化
// ======================================
ZMALoader.setOnLoad(function (mapOptions, error) {
  if (error) {
    console.error('地図の初期化エラー:', error);
    return;
  }
  const initialLat = 35.681236, initialLng = 139.767125;
  mapOptions.center = new ZDC.LatLng(initialLat, initialLng);
  mapOptions.zoom = 15;

  map = new ZDC.Map(
    document.getElementById('ZMap'),
    mapOptions,
    () => {
      console.log('地図の初期化が完了しました');
      map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
      map.addControl(new ZDC.Compass('top-right'));
      map.addControl(new ZDC.ScaleBar('bottom-left'));
      // 中心点マーカーを追加
      const centerMarker = new ZDC.CenterMarker();
      map.addControl(centerMarker);
    },
    () => console.error('地図の初期化に失敗しました')
  );
});

// ======================================
// クイック検索
// ======================================
function quickSearch(keyword, genreCode) {
  document.getElementById('searchInput').value = '';
  searchPOI(genreCode, keyword);
}

// ======================================
// 施設検索
// ======================================
function searchPOI(genreCode, displayKeyword) {
  const searchInput = document.getElementById('searchInput');
  const keyword = searchInput.value.trim();

  if (!keyword && !genreCode) {
    alert('検索キーワードを入力してください');
    return;
  }

  const logKeyword = displayKeyword || keyword;
  console.log(`施設検索: ${logKeyword}`);

  const center = map.getCenter();
  const lat = center.lat, lng = center.lng;

  const params = new URLSearchParams({
    genre_pattern: '101',
    proximity: `${lng},${lat},2000`,
    limit: '0,30',
    datum: 'JGD'
  });

  if (genreCode) {
    params.append('genre_code', genreCode);
  } else if (keyword) {
    params.append('word', keyword);
    params.append('word_match_type', '3');
  }

  const requestUrl = `${POI_SEARCH_URL}?${params.toString()}`;
  console.log('リクエストURL:', requestUrl);

  const xhr = new XMLHttpRequest();
  xhr.open('GET', requestUrl);
  xhr.setRequestHeader('x-api-key', API_KEY);
  xhr.setRequestHeader('Authorization', 'referer');

  xhr.onload = function() {
    if (this.status === 200) {
      try {
        handleSearchResponse(JSON.parse(this.responseText));
      } catch (e) {
        console.error('レスポンス解析エラー:', e);
        alert('検索結果の解析に失敗しました');
      }
    } else {
      console.error('APIエラー:', this.status, this.responseText);
      alert(`検索に失敗 (ステータス: ${this.status})`);
    }
  };
  xhr.onerror = () => alert('ネットワークエラー');
  xhr.send();
}

// ======================================
// 検索結果処理
// ======================================
function handleSearchResponse(response) {
  if (response.status !== 'OK') {
    alert(`検索エラー: ${response.message || '不明なエラー'}`);
    return;
  }
  const items = response.result.item || [];
  const hitCount = response.result.info.hit || 0;
  if (items.length === 0) {
    alert('検索結果が見つかりませんでした');
    return;
  }
  currentResults = items;
  clearMarkers();
  displayResultsOnMap(items);
  displayResultsList(items, hitCount);
}

// ======================================
// 地図にマーカー表示
// ======================================
function displayResultsOnMap(items) {
  items.forEach((item, index) => {
    if (item.position?.length === 2) {
      const [lng, lat] = item.position;
      const marker = new ZDC.Marker(new ZDC.LatLng(lat, lng), {
        styleId: ZDC.MARKER_COLOR_ID_BLUE_S,
        title: item.name || '施設名不明'
      });
      marker.addEventListener('click', () => showInfoWindow(item));
      map.addWidget(marker);
      markers.push(marker);
    }
  });
  if (items[0]?.position) {
    map.setCenter(new ZDC.LatLng(items[0].position[1], items[0].position[0]));
  }
}

// ======================================
// 結果リスト表示
// ======================================
function displayResultsList(items, hitCount) {
  const container = document.getElementById('result-container');
  let html = `<div class="result-count">検索結果: ${hitCount}件</div>`;
  items.forEach((item, i) => {
    const name = item.name || '施設名不明';
    const address = item.address || '住所情報なし';
    const distance = item.distance ? formatDistance(item.distance) : '距離不明';
    html += `
      <div class="result-item" onclick="focusMarker(${i})">
        <div class="result-name">${i+1}. ${name}</div>
        <div class="result-address">${address}</div>
        <div class="result-distance">📍 ${distance}</div>
      </div>
    `;
  });
  container.innerHTML = html;
  container.style.display = 'block';
}

// ======================================
// 距離フォーマット
// ======================================
function formatDistance(distance) {
  return distance >= 1000
    ? (distance / 1000).toFixed(1) + ' km'
    : Math.round(distance) + ' m';
}

// ======================================
// マーカーにフォーカス
// ======================================
function focusMarker(index) {
  const item = currentResults[index];
  if (item?.position) {
    map.setCenter(new ZDC.LatLng(item.position[1], item.position[0]));
    map.setZoom(17);
    showInfoWindow(item);
  }
}

// ======================================
// 情報ウィンドウ表示
// ======================================
function showInfoWindow(item) {
  if (currentPopup) {
    map.removeWidget(currentPopup);
    currentPopup = null;
  }
  const html = `
    <div style="padding:10px;min-width:200px;max-width:300px;">
      <h3 style="margin:0 0 10px 0;font-size:16px;color:#333;border-bottom:2px solid #007bff;padding-bottom:5px;">
        ${item.name || '施設名不明'}
      </h3>
      <div style="font-size:13px;line-height:1.6;color:#555;">
        <p><strong>📍 距離:</strong> ${formatDistance(item.distance || 0)}</p>
        <p><strong>🏠 住所:</strong><br>${item.address || '住所情報なし'}</p>
        <p><strong>📞 電話:</strong> ${item.phone_number || '電話番号なし'}</p>
        <p><strong>🏷️ ジャンル:</strong> ${item.main_genre_name || 'ジャンル不明'}</p>
      </div>
    </div>`;
  currentPopup = new ZDC.Popup(new ZDC.LatLng(item.position[1], item.position[0]), {
    htmlSource: html,
    closeButton: true,
    offset: new ZDC.Point(0, -40)
  });
  map.addWidget(currentPopup);
  currentPopup.show();
}

// ======================================
// マーカークリア
// ======================================
function clearMarkers() {
  markers.forEach(m => map.removeWidget(m));
  markers = [];
  if (currentPopup) {
    map.removeWidget(currentPopup);
    currentPopup = null;
  }
}

// ======================================
// Enterキー検索
// ======================================
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('searchInput')?.addEventListener('keypress', e => {
    if (e.key === 'Enter') searchPOI();
  });
});

コードを実行した結果は、以下になります。
初期表示
poi_search1.png

フリーワードで検索した結果
poi_search_word.png

ジャンルで検索した結果
poi_search_genre.png

ステップ毎の解説

Step 1:HTML(UI)を用意する

<body>
  <!-- 検索フォーム -->
  <div id="search-box">
    <input type="text" id="keyword" placeholder="施設名を入力 (例: コンビニ)">
    <button id="search-btn">検索</button>
  </div>

  <!-- クイック検索ボタン(ジャンルコード指定) -->
  <div id="quick-buttons">
    <button class="quick-btn" data-genre="00140:0014000180">コンビニ</button>
    <button class="quick-btn" data-genre="00150:0015000080">レストラン</button>
    <button class="quick-btn" data-genre="00160:0016000050">カフェ</button>
  </div>

  <!-- 検索結果リスト -->
  <div id="result-list">
    <ul id="results"></ul>
  </div>

  <!-- 地図表示領域 -->
  <div id="ZMap" style="width: 100%; height: 500px;"></div>
</body>
  • 検索入力(テキスト)と「検索」ボタン
  • クイックボタン(よく使うジャンルをジャンルコードで発火)
  • 結果表示エリア(リスト)と地図領域(#ZMap
  • ポイント:ジャンルコードは公式のジャンルコード一覧から取り、genre_code パラメータで投げます(例:コンビニ 00140:0014000180)。ジャンル一覧を必ず確認してください。

Step 2:地図の初期化(ZMALoader)

ZMALoader.setOnLoad(function(mapOptions, error){
  if (error) { console.error(error); return; }
  mapOptions.center = new ZDC.LatLng(35.681236, 139.767125);
  mapOptions.zoom = 15;
  map = new ZDC.Map(document.getElementById('ZMap'), mapOptions, () => {
      console.log('地図の初期化が完了しました');
      map.addControl(new ZDC.ZoomButton('bottom-right', new ZDC.Point(-20, -35)));
      map.addControl(new ZDC.Compass('top-right'));
      map.addControl(new ZDC.ScaleBar('bottom-left'));
      // 中心点マーカーを追加
      const centerMarker = new ZDC.CenterMarker();
      map.addControl(centerMarker);
    });
});
  • ZMALoader.setOnLoad(function(mapOptions, error){ ... }) で map を生成します。
  • 初期 center / zoom を設定し、必要なコントロール(ZoomButton, ScaleBar, Compass)を追加します。

Step 3:検索リクエストを作る

例(2km以内、部分一致、最大30件)

const params = new URLSearchParams({
  genre_pattern: '101',
  proximity: `${lng},${lat},2000`,
  limit: '0,30',
  datum: 'JGD'
});
params.append('word', keyword); // または params.append('genre_code', '00140:0014000180');
params.append('word_match_type', '3');
  • 必須genre_pattern(101:コンシューマー or 102:業務) — ジャンルパターンを指定。
  • proximity: "経度,緯度,半径(m)"(代表点の近傍検索。最大半径5000m)。
  • キーワード検索は word(複数ワードは空白で AND)+ word_match_type(1=完全,2=前方,3=部分)で制御。
  • ジャンル検索は genre_codeレベル1:レベル2 を渡す(複数可、カンマ区切り)。

Step 4:API呼び出し(HTTP・認証)

const url = `${POI_SEARCH_URL}?${params.toString()}`;
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.setRequestHeader('x-api-key', API_KEY);
xhr.setRequestHeader('Authorization', 'referer'); // 認証方式による
xhr.onload = () => { if (xhr.status===200) handleSearchResponse(JSON.parse(xhr.responseText)); };
xhr.send();
  • GET/POST可能(サンプルでは GET
  • 必要ヘッダ:x-api-key: <API_KEY> と、リファラ認証の場合 Authorization: referer 等(コンソールで設定した認証方式に合わせる)。

Step 5:レスポンスを扱う

function handleSearchResponse(resp){
  if (resp.status!=='OK') { /* エラー処理 */ }
  const items = resp.result.item || [];
  // items[i].position => [lng, lat]
}
  • 返却:response.result.item[](各 item に position([lng,lat])や nameaddressdistancemain_genre_name 等がある)。
  • 必ず position があるか確認してからマーカーを作る。
  • response.result.info.hit でヒット件数を取得可能。

Step 6:マーカー表示・Popup

// ======================================
// 検索結果をマップにマーカーで表示
// ======================================
function displayResultsOnMap(items) {
  items.forEach((item, index) => {
    if (item.position && item.position.length === 2) {
      const lat = item.position[1];
      const lng = item.position[0];

      // マーカーを作成
      const marker = new ZDC.Marker(
        new ZDC.LatLng(lat, lng),
        {
          styleId: ZDC.MARKER_COLOR_ID_RED_S, // 赤いマーカー
          title: item.name || '施設名不明'
        }
      );

      // マーカークリックイベント → Popup表示
      marker.addEventListener('click', function() {
        showInfoWindow(item);
      });

      // 地図に追加
      map.addWidget(marker);
      markers.push(marker);
    }
  });
}

// ======================================
// Popupを表示
// ======================================
function showInfoWindow(item) {
  // 既存のPopupを削除
  if (currentPopup) {
    map.removeWidget(currentPopup);
    currentPopup = null;
  }

  const name = item.name || '施設名不明';
  const address = item.address || '住所情報なし';

  const htmlContent = `
    <div style="padding: 10px; min-width: 200px;">
      <h3 style="margin:0 0 10px; font-size:14px; color:#333;">
        ${name}
      </h3>
      <p style="margin: 5px 0; font-size:12px; color:#555;">
        📍 ${address}
      </p>
    </div>
  `;

  currentPopup = new ZDC.Popup(
    new ZDC.LatLng(item.position[1], item.position[0]),
    {
      htmlSource: htmlContent,
      closeButton: true,
      offset: new ZDC.Point(0, -40) // マーカーと重ならないように上にずらす
    }
  );

  map.addWidget(currentPopup);
  currentPopup.show();
}

おわりに

今回は施設検索APIを使って、検索結果を地図上にマーカーと吹き出しで表示し、さらに過去の結果をクリアして再検索できる仕組みを実装しました。
これができるようになると、観光スポット検索や店舗一覧の可視化、業務用の拠点検索など、さまざまな場面で応用できます。

「地図と検索を組み合わせると、こんなに便利になるんだ!」と感じてもらえたら嬉しいです。
ぜひ自分のプロジェクトにも取り入れてみてください。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?