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-11-14

はじめに

地図を使ったサービスでは、「この場所は対象エリア内なのか?」という判定が欠かせません。
たとえば、配送・訪問・営業など、さまざまな業務で使われています。

本記事では、ZENRIN Maps API の郵便番号検索機能を活用して、
地図上で簡単にエリア判定を行う 「配送エリア判定システム」 を作ってみます。

郵便番号を入力すると、該当エリアの位置を地図上に表示し、
対象エリア内かどうかをメッセージでわかりやすく知らせてくれます。

たとえば、

  • 配送業務で「どの範囲まで届けられるか」知りたい
  • 新しい拠点の商圏をざっくり把握したい
  • 配送スタッフが現場で確認できる簡易ツールを作りたい

といったシーンで、そのまま応用できる仕組みです。

HTML・CSS・JavaScript だけで動作するため、
地図APIを初めて触る方でも簡単に試せます。

この記事でできること

  • ZENRIN Maps API を使って地図を表示
  • 郵便番号から住所検索
  • 緯度経度、半径から郵便番号検索
  • 住所コードから郵便番号検索
  • 検索結果の各住所をマーカーで地図上に表示

APIキーの取得手順

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

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

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

ZENRIN Maps APIの始め方

公式リファレンス

ファイル構成

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

サンプルコード

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ZENRIN Maps API - 郵便番号検索デモ</title>
  <link rel="stylesheet" href="css/zma_postcode_search.css">
  <script src="https://test-js.zmaps-api.com/zma_loader.js?key=[APIキー]&auth=referer"></script>
</head>
<body>
  <!-- ヘッダー -->
  <div class="header">
    <h1>🚚 配送エリア判定システム</h1>
    <p>ZENRIN Maps API を使用した配送可能エリアチェックシステム</p>
  </div>

  <!-- メインコンテナ -->
  <div class="main-container">
    <!-- サイドバー -->
    <div class="sidebar">
      <!-- タブナビゲーション -->
      <div class="tab-navigation">
        <button class="tab-button active" onclick="switchTab('delivery')">配送エリア判定</button>
        <button class="tab-button" onclick="switchTab('area')">エリア調査</button>
        <button class="tab-button" onclick="switchTab('management')">配送拠点管理</button>
      </div>

      <!-- 配送エリア判定タブ -->
      <div id="delivery-tab" class="tab-content active">
        <div class="search-panel">
          <h3>🚚 配送エリア判定</h3>
          <p class="panel-description">お客様の郵便番号を入力して配送可能エリアかチェックします</p>
          
          <div class="form-group">
            <label for="postcodeInput">配送先郵便番号</label>
            <input type="text" id="postcodeInput" placeholder="100-0001 または 1000001" maxlength="8">
            <small class="form-help">ハイフンありなしどちらでも入力可能です</small>
          </div>
          
          <div class="form-group">
            <label for="deliveryService">配送サービス</label>
            <select id="deliveryService">
              <option value="standard">標準配送(全国対応)</option>
              <option value="express">速達配送(主要都市のみ)</option>
              <option value="same_day">当日配送(東京23区のみ)</option>
            </select>
          </div>
          
          <div class="search-buttons">
            <button class="btn btn-primary" onclick="checkDeliveryArea()">配送可否判定</button>
            <button class="btn btn-secondary" onclick="clearDeliverySearch()">クリア</button>
          </div>
        </div>
      </div>

      <!-- エリア調査タブ -->
      <div id="area-tab" class="tab-content">
        <div class="search-panel">
          <h3>📍 エリア調査</h3>
          <p class="panel-description">指定した拠点周辺の配送エリアを調査します</p>
          
          <div class="form-row">
            <div class="form-group">
              <label for="proximityLat">拠点緯度</label>
              <input type="number" id="proximityLat" placeholder="35.681236" step="0.000001">
            </div>
            <div class="form-group">
              <label for="proximityLng">拠点経度</label>
              <input type="number" id="proximityLng" placeholder="139.767125" step="0.000001">
            </div>
          </div>
          
          <div class="form-group">
            <label for="proximityRadius">調査半径(m)</label>
            <input type="number" id="proximityRadius" placeholder="2000" min="500" max="5000">
            <small class="form-help">500m〜5000mの範囲で設定してください</small>
          </div>
          
          <div class="form-group">
            <label for="areaType">調査タイプ</label>
            <select id="areaType">
              <option value="all">全エリア調査</option>
              <option value="residential">住宅エリア中心</option>
              <option value="commercial">商業エリア中心</option>
            </select>
          </div>
          
          <div class="search-buttons">
            <button class="btn btn-primary" onclick="searchByProximity()">エリア調査開始</button>
            <button class="btn btn-secondary" onclick="clearProximitySearch()">クリア</button>
          </div>
        </div>
      </div>

      <!-- 配送拠点管理タブ -->
      <div id="management-tab" class="tab-content">
        <div class="search-panel">
          <h3>🏢 配送拠点管理</h3>
          <p class="panel-description">配送拠点のエリア設定と管理を行います</p>
          
          <div class="form-group">
            <label for="addressCodeInput">拠点エリアコード</label>
            <input type="text" id="addressCodeInput" placeholder="13101(東京都千代田区)" maxlength="8">
            <small class="form-help">都道府県・市区町村レベルのコードを入力</small>
          </div>
          
          <div class="form-group">
            <label for="hubName">拠点名</label>
            <input type="text" id="hubName" placeholder="東京配送センター" maxlength="50">
          </div>
          
          <div class="form-group">
            <label for="deliveryCapacity">配送能力(件/日)</label>
            <input type="number" id="deliveryCapacity" placeholder="1000" min="1" max="10000">
          </div>
          
          <div class="search-buttons">
            <button class="btn btn-primary" onclick="searchByAddressCode()">拠点エリア確認</button>
            <button class="btn btn-secondary" onclick="clearAddressSearch()">クリア</button>
          </div>
        </div>
      </div>

      <!-- 結果パネル -->
      <div class="results-panel">
        <div class="results-header">
          <div class="results-count" id="resultsCount">配送エリア: 0件</div>
          <button class="clear-results" onclick="clearResults()" style="display: none;">結果をクリア</button>
        </div>
        <div id="resultsList">
          <div style="text-align: center; color: #999; padding: 20px;">
            配送先郵便番号を入力して配送可否を確認してください
          </div>
        </div>
      </div>
    </div>

    <!-- 地図エリア -->
    <div class="map-container">
      <div id="ZMap"></div>
    </div>
  </div>

  <script src="js/zma_postcode_search.js"></script>
</body>
</html>
CSS(クリックで展開)
zma_postcode_search.css
    body { 
      margin: 0; 
      padding: 0; 
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 
      background-color: #f5f5f5;
    }
    
    /* ヘッダー */
    .header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 12px 20px;
      text-align: center;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    
    .header h1 {
      margin: 0;
      font-size: 22px;
      font-weight: 300;
    }
    
    .header p {
      margin: 5px 0 0 0;
      opacity: 0.9;
      font-size: 14px;
    }
    
    /* メインコンテナ */
    .main-container {
      display: flex;
      height: calc(100vh - 80px);
    }
    
    /* サイドバー */
    .sidebar {
      width: 400px;
      background: white;
      box-shadow: 2px 0 10px rgba(0,0,0,0.1);
      overflow-y: auto;
      z-index: 1000;
      display: flex;
      flex-direction: column;
    }
    
    .search-panel {
      padding: 12px;
      border-bottom: 1px solid #eee;
      flex-shrink: 0;
    }
    
    .search-panel h3 {
      margin: 0 0 8px 0;
      color: #333;
      font-size: 15px;
      display: flex;
      align-items: center;
      gap: 6px;
    }
    
    .form-group {
      margin-bottom: 10px;
    }
    
    .form-group label {
      display: block;
      margin-bottom: 4px;
      font-weight: 600;
      color: #555;
      font-size: 13px;
    }
    
    .form-group input,
    .form-group select {
      width: 100%;
      padding: 8px;
      border: 2px solid #e1e5e9;
      border-radius: 6px;
      font-size: 13px;
      transition: border-color 0.3s;
      box-sizing: border-box;
    }
    
    .form-group input:focus,
    .form-group select:focus {
      outline: none;
      border-color: #667eea;
    }
    
    .panel-description {
      font-size: 11px;
      color: #666;
      margin-bottom: 8px;
      line-height: 1.3;
    }
    
    .form-help {
      font-size: 11px;
      color: #888;
      margin-top: 4px;
      display: block;
    }
    
    .form-row {
      display: flex;
      gap: 10px;
    }
    
    .form-row .form-group {
      flex: 1;
    }
    
    .search-buttons {
      display: flex;
      gap: 8px;
      margin-top: 12px;
    }
    
    .btn {
      padding: 8px 16px;
      border: none;
      border-radius: 6px;
      font-size: 12px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s;
      flex: 1;
    }
    
    .btn-primary {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
    }
    
    .btn-primary:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
    }
    
    .btn-secondary {
      background: #f8f9fa;
      color: #6c757d;
      border: 2px solid #e9ecef;
    }
    
    .btn-secondary:hover {
      background: #e9ecef;
    }
    
    .btn:disabled {
      opacity: 0.6;
      cursor: not-allowed;
      transform: none;
    }
    
    /* タブナビゲーション */
    .tab-navigation {
      display: flex;
      background: #f8f9fa;
      border-bottom: 1px solid #e9ecef;
      flex-shrink: 0;
    }
    
    .tab-button {
      flex: 1;
      padding: 10px 14px;
      background: none;
      border: none;
      cursor: pointer;
      font-size: 13px;
      font-weight: 600;
      color: #6c757d;
      transition: all 0.3s;
    }
    
    .tab-button.active {
      background: white;
      color: #667eea;
      border-bottom: 3px solid #667eea;
    }
    
    .tab-content {
      display: none;
      padding: 12px;
      flex-shrink: 0;
    }
    
    .tab-content.active {
      display: block;
    }
    
    /* 検索結果パネル */
    .results-panel {
      padding: 12px;
      max-height: 350px;
      overflow-y: auto;
      flex: 1;
      min-height: 0;
    }
    
    .results-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 10px;
      padding-bottom: 8px;
      border-bottom: 2px solid #e1e5e9;
    }
    
    .results-count {
      font-weight: 600;
      color: #667eea;
      font-size: 14px;
    }
    
    .clear-results {
      background: #dc3545;
      color: white;
      border: none;
      padding: 6px 12px;
      border-radius: 4px;
      font-size: 12px;
      cursor: pointer;
    }
    
    .result-item {
      padding: 12px;
      margin-bottom: 8px;
      background: white;
      border: 1px solid #e1e5e9;
      border-radius: 8px;
      cursor: pointer;
      transition: all 0.3s;
      box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    }
    
    .result-item:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      border-color: #667eea;
    }
    
    .result-postcode {
      font-weight: 600;
      color: #667eea;
      font-size: 16px;
      margin-bottom: 4px;
    }
    
    .result-address {
      color: #333;
      font-size: 13px;
      margin-bottom: 4px;
    }
    
    .result-details {
      color: #666;
      font-size: 11px;
      margin-top: 6px;
    }
    
    .result-distance {
      color: #28a745;
      font-size: 11px;
      font-weight: 600;
      margin-top: 4px;
    }
    
    /* 配送エリア判定結果のスタイル */
    .delivery-result {
      padding: 12px;
      margin-bottom: 8px;
      border-radius: 8px;
      border-left: 4px solid;
    }
    
    .delivery-available {
      background: #d4edda;
      border-left-color: #28a745;
      color: #155724;
    }
    
    .delivery-unavailable {
      background: #f8d7da;
      border-left-color: #dc3545;
      color: #721c24;
    }
    
    .delivery-status {
      font-weight: 600;
      font-size: 14px;
      margin-bottom: 6px;
    }
    
    .delivery-info {
      font-size: 13px;
      line-height: 1.3;
    }
    
    .delivery-service {
      background: #e9ecef;
      padding: 3px 6px;
      border-radius: 10px;
      font-size: 11px;
      font-weight: 600;
      display: inline-block;
      margin-top: 4px;
    }
    
    /* 地図エリア */
    .map-container {
      flex: 1;
      position: relative;
    }
    
    #ZMap {
      width: 100%;
      height: 100%;
    }
    
    /* ローディング */
    .loading {
      text-align: center;
      padding: 40px;
      color: #666;
    }
    
    .spinner {
      border: 3px solid #f3f3f3;
      border-top: 3px solid #667eea;
      border-radius: 50%;
      width: 30px;
      height: 30px;
      animation: spin 1s linear infinite;
      margin: 0 auto 15px;
    }
    
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    
    /* エラーメッセージ */
    .error {
      background: #f8d7da;
      color: #721c24;
      padding: 15px;
      border-radius: 6px;
      border: 1px solid #f5c6cb;
      margin: 10px 0;
    }
    
    /* レスポンシブ */
    @media (max-width: 768px) {
      .main-container {
        flex-direction: column;
        height: auto;
      }
      
      .sidebar {
        width: 100%;
        max-height: 50vh;
      }
      
      .map-container {
        height: 50vh;
      }
    }
zma_postcode_search.js
    // ======================================
    // グローバル変数
    // ======================================
    let map; // 地図オブジェクト
    let markers = []; // マーカーを格納する配列
    let currentResults = []; // 現在の検索結果を格納
    let currentPopup = null; // 現在表示中のPopup

    // APIの設定
    const API_KEY = 'APIキー';
    const POSTCODE_SEARCH_URL = 'https://test-web.zmaps-api.com/search/postcode';

    // ======================================
    // 地図の初期化
    // ======================================
    ZMALoader.setOnLoad(function (mapOptions, error) {
      if (error) {
        console.error('地図の初期化エラー:', error);
        return;
      }

      // 初期表示位置(東京駅周辺)
      const initialLat = 35.681236;
      const initialLng = 139.767125;

      // 地図の中心を設定
      mapOptions.center = new ZDC.LatLng(initialLat, initialLng);
      mapOptions.zoom = 15;

      // 地図を生成
      map = new ZDC.Map(
        document.getElementById('ZMap'),
        mapOptions,
        function () {
          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'));
          
          // 初期座標をフォームに設定
          document.getElementById('proximityLat').value = initialLat;
          document.getElementById('proximityLng').value = initialLng;
        },
        function () {
          console.error('地図の初期化に失敗しました');
        }
      );
    });

    // ======================================
    // タブ切り替え
    // ======================================
    function switchTab(tabName) {
      // すべてのタブボタンとコンテンツを非アクティブに
      document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
      document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
      
      // 選択されたタブをアクティブに
      document.querySelector(`[onclick="switchTab('${tabName}')"]`).classList.add('active');
      document.getElementById(`${tabName}-tab`).classList.add('active');
      
      // 結果表示エリアのメッセージを更新
      updateResultsMessage(tabName);
    }
    
    // ======================================
    // 結果表示メッセージの更新
    // ======================================
    function updateResultsMessage(tabName) {
      const resultsList = document.getElementById('resultsList');
      const resultsCount = document.getElementById('resultsCount');
      
      let message = '';
      let countText = '';
      
      switch(tabName) {
        case 'delivery':
          message = '配送先郵便番号を入力して配送可否を確認してください';
          countText = '配送エリア: 0件';
          break;
        case 'area':
          message = '拠点座標を入力してエリア調査を開始してください';
          countText = '調査エリア: 0件';
          break;
        case 'management':
          message = '拠点エリアコードを入力して拠点情報を確認してください';
          countText = '拠点エリア: 0件';
          break;
      }
      
      resultsList.innerHTML = `<div style="text-align: center; color: #999; padding: 20px;">${message}</div>`;
      resultsCount.textContent = countText;
    }

    // ======================================
    // 配送エリア判定
    // ======================================
    function checkDeliveryArea() {
      const postcode = document.getElementById('postcodeInput').value.trim();
      const deliveryService = document.getElementById('deliveryService').value;

      if (!postcode) {
        alert('配送先郵便番号を入力してください');
        return;
      }

      console.log('配送エリア判定を実行:', { postcode, deliveryService });

      // 検索パラメータを設定
      const params = new URLSearchParams({
        post_code: postcode,
        sort: 'post_code',
        limit: '0,10',
        datum: 'JGD'
      });

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

      // ローディング表示
      showLoading();

      // XMLHttpRequestでAPI呼び出し
      const xhr = new XMLHttpRequest();
      xhr.open('GET', requestUrl);
      xhr.setRequestHeader('x-api-key', API_KEY);
      xhr.setRequestHeader('Authorization', 'referer');

      xhr.onload = function() {
        hideLoading();
        
        if (this.status === 200) {
          try {
            const response = JSON.parse(this.responseText);
            handleDeliveryResponse(response, deliveryService);
          } catch (e) {
            console.error('レスポンスの解析エラー:', e);
            showError('配送エリア判定に失敗しました');
          }
        } else {
          console.error('APIエラー:', this.status, this.responseText);
          showError(`配送エリア判定に失敗しました (ステータス: ${this.status})`);
        }
      };

      xhr.onerror = function() {
        hideLoading();
        console.error('ネットワークエラー');
        showError('ネットワークエラーが発生しました');
      };

      xhr.send();
    }

    // ======================================
    // 配送エリア判定結果の処理
    // ======================================
    function handleDeliveryResponse(response, deliveryService) {
      console.log('配送エリア判定結果:', response);

      if (response.status !== 'OK') {
        showError(`配送エリア判定エラー: ${response.message || '不明なエラー'}`);
        return;
      }

      const items = response.result.item || [];
      const hitCount = response.result.info.hit || 0;

      console.log(`配送エリア判定結果: ${hitCount}件`);

      if (items.length === 0) {
        showDeliveryUnavailable('指定された郵便番号は配送対象外です');
        return;
      }

      // 配送可否を判定
      const isDeliverable = checkDeliveryAvailability(items[0], deliveryService);
      
      // 現在の検索結果を保存
      currentResults = items;

      // 既存のマーカーをクリア
      clearMarkers();

      // 検索結果を地図上に表示
      displayResultsOnMap(items);

      // 配送結果を表示
      displayDeliveryResult(items[0], isDeliverable, deliveryService);
    }

    // ======================================
    // 配送可否の判定ロジック
    // ======================================
    function checkDeliveryAvailability(item, deliveryService) {
      const addressCode = item.address_code || '';
      const prefectureCode = addressCode.substring(0, 2);
      
      // 配送サービス別の判定ロジック
      switch(deliveryService) {
        case 'standard':
          // 標準配送:全国対応
          return true;
        case 'express':
          // 速達配送:主要都市のみ(都道府県コードで判定)
          const expressAreas = ['13', '14', '27', '28', '40', '41', '43', '45']; // 東京、神奈川、大阪、兵庫、福岡、佐賀、熊本、大分
          return expressAreas.includes(prefectureCode);
        case 'same_day':
          // 当日配送:東京23区のみ
          return addressCode.startsWith('13101'); // 千代田区のコード
        default:
          return true;
      }
    }

    // ======================================
    // 配送結果の表示
    // ======================================
    function displayDeliveryResult(item, isDeliverable, deliveryService) {
      const resultsList = document.getElementById('resultsList');
      const resultsCount = document.getElementById('resultsCount');
      const clearButton = document.querySelector('.clear-results');
      
      resultsCount.textContent = `配送エリア: 1件`;
      clearButton.style.display = 'block';

      const postcode = item.post_code || '郵便番号不明';
      const address = item.address || '住所情報なし';
      const serviceName = getDeliveryServiceName(deliveryService);

      let html = '';
      
      if (isDeliverable) {
        html = `
          <div class="delivery-result delivery-available">
            <div class="delivery-status">✅ 配送可能</div>
            <div class="delivery-info">
              <strong>${postcode}</strong><br>
              ${address}<br>
              <span class="delivery-service">${serviceName}</span>
            </div>
          </div>
        `;
      } else {
        html = `
          <div class="delivery-result delivery-unavailable">
            <div class="delivery-status">❌ 配送不可</div>
            <div class="delivery-info">
              <strong>${postcode}</strong><br>
              ${address}<br>
              <span class="delivery-service">${serviceName}</span><br>
              <small>このエリアは${serviceName}の配送対象外です</small>
            </div>
          </div>
        `;
      }

      resultsList.innerHTML = html;
    }

    // ======================================
    // 配送サービス名の取得
    // ======================================
    function getDeliveryServiceName(serviceCode) {
      const serviceNames = {
        'standard': '標準配送',
        'express': '速達配送',
        'same_day': '当日配送'
      };
      return serviceNames[serviceCode] || '配送サービス';
    }

    // ======================================
    // 配送不可の表示
    // ======================================
    function showDeliveryUnavailable(message) {
      const resultsList = document.getElementById('resultsList');
      resultsList.innerHTML = `
        <div class="delivery-result delivery-unavailable">
          <div class="delivery-status">❌ 配送不可</div>
          <div class="delivery-info">${message}</div>
        </div>
      `;
    }

    // ======================================
    // 郵便番号検索(従来の機能)
    // ======================================
    function searchByPostcode() {
      const postcode = document.getElementById('postcodeInput').value.trim();
      const sort = document.getElementById('postcodeSort').value;

      if (!postcode) {
        alert('郵便番号を入力してください');
        return;
      }

      console.log('郵便番号検索を実行:', postcode);

      // 検索パラメータを設定
      const params = new URLSearchParams({
        post_code: postcode,
        sort: sort,
        limit: '0,50',
        datum: 'JGD'
      });

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

      // ローディング表示
      showLoading();

      // XMLHttpRequestでAPI呼び出し
      const xhr = new XMLHttpRequest();
      xhr.open('GET', requestUrl);
      xhr.setRequestHeader('x-api-key', API_KEY);
      xhr.setRequestHeader('Authorization', 'referer');

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

      xhr.onerror = function() {
        hideLoading();
        console.error('ネットワークエラー');
        showError('ネットワークエラーが発生しました');
      };

      xhr.send();
    }

    // ======================================
    // 近隣郵便番号検索
    // ======================================
    function searchByProximity() {
      const lat = parseFloat(document.getElementById('proximityLat').value);
      const lng = parseFloat(document.getElementById('proximityLng').value);
      const radius = parseInt(document.getElementById('proximityRadius').value) || 1000;

      if (isNaN(lat) || isNaN(lng)) {
        alert('有効な緯度・経度を入力してください');
        return;
      }

      console.log('近隣郵便番号検索を実行:', { lat, lng, radius });

      // 検索パラメータを設定
      const params = new URLSearchParams({
        proximity: `${lng},${lat},${radius}`,
        sort: 'distance',
        limit: '0,50',
        datum: 'JGD'
      });

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

      // ローディング表示
      showLoading();

      // XMLHttpRequestでAPI呼び出し
      const xhr = new XMLHttpRequest();
      xhr.open('GET', requestUrl);
      xhr.setRequestHeader('x-api-key', API_KEY);
      xhr.setRequestHeader('Authorization', 'referer');

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

      xhr.onerror = function() {
        hideLoading();
        console.error('ネットワークエラー');
        showError('ネットワークエラーが発生しました');
      };

      xhr.send();
    }

    // ======================================
    // 住所コード検索
    // ======================================
    function searchByAddressCode() {
      const addressCode = document.getElementById('addressCodeInput').value.trim();

      if (!addressCode) {
        alert('住所コードを入力してください');
        return;
      }

      console.log('住所コード検索を実行:', addressCode);

      // 検索パラメータを設定
      const params = new URLSearchParams({
        address_code_list: addressCode,
        sort: 'post_code',
        limit: '0,50',
        datum: 'JGD'
      });

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

      // ローディング表示
      showLoading();

      // XMLHttpRequestでAPI呼び出し
      const xhr = new XMLHttpRequest();
      xhr.open('GET', requestUrl);
      xhr.setRequestHeader('x-api-key', API_KEY);
      xhr.setRequestHeader('Authorization', 'referer');

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

      xhr.onerror = function() {
        hideLoading();
        console.error('ネットワークエラー');
        showError('ネットワークエラーが発生しました');
      };

      xhr.send();
    }

    // ======================================
    // 検索結果の処理
    // ======================================
    function handleSearchResponse(response) {
      console.log('検索結果:', response);

      if (response.status !== 'OK') {
        showError(`検索エラー: ${response.message || '不明なエラー'}`);
        return;
      }

      const items = response.result.item || [];
      const hitCount = response.result.info.hit || 0;

      console.log(`検索結果: ${hitCount}件`);

      if (items.length === 0) {
        showError('検索結果が見つかりませんでした');
        return;
      }

      // 現在の検索結果を保存
      currentResults = items;

      // 既存のマーカーをクリア
      clearMarkers();

      // 検索結果を地図上に表示
      displayResultsOnMap(items);

      // 検索結果をリスト表示
      displayResultsList(items, hitCount);
    }

    // ======================================
    // 検索結果を地図上にマーカーで表示
    // ======================================
    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_BLUE_S, // 青いマーカー
              title: item.post_code || '郵便番号不明'
            }
          );

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

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

      // 最初の結果に地図の中心を移動
      if (items.length > 0 && items[0].position) {
        const firstItem = items[0];
        map.setCenter(new ZDC.LatLng(firstItem.position[1], firstItem.position[0]));
      }
    }

    // ======================================
    // 検索結果をリスト表示
    // ======================================
    function displayResultsList(items, hitCount) {
      const resultsList = document.getElementById('resultsList');
      const resultsCount = document.getElementById('resultsCount');
      const clearButton = document.querySelector('.clear-results');
      
      resultsCount.textContent = `検索結果: ${hitCount}件`;
      clearButton.style.display = 'block';

      let html = '';

      items.forEach((item, index) => {
        const postcode = item.post_code || '郵便番号不明';
        const address = item.address || '住所情報なし';
        const addressRead = item.address_read || '';
        const postLevel = item.post_level || '';
        const postType = item.post_type || '';
        const distance = item.distance ? formatDistance(item.distance) : '';

        html += `
          <div class="result-item" onclick="focusMarker(${index})">
            <div class="result-postcode">${postcode}</div>
            <div class="result-address">${address}</div>
            <div class="result-details">
              <div>読み: ${addressRead}</div>
              <div>レベル: ${postLevel} | 種別: ${postType}</div>
              ${distance ? `<div class="result-distance">📍 ${distance}</div>` : ''}
            </div>
          </div>
        `;
      });

      resultsList.innerHTML = html;
    }

    // ======================================
    // 距離を見やすい形式にフォーマット
    // ======================================
    function formatDistance(distance) {
      if (distance >= 1000) {
        return (distance / 1000).toFixed(1) + ' km';
      } else {
        return Math.round(distance) + ' m';
      }
    }

    // ======================================
    // マーカーにフォーカス(地図の中心を移動)
    // ======================================
    function focusMarker(index) {
      const item = currentResults[index];
      if (item && item.position) {
        const lat = item.position[1];
        const lng = item.position[0];
        
        // 地図の中心を移動
        map.setCenter(new ZDC.LatLng(lat, lng));
        map.setZoom(17);

        // 情報ウィンドウを表示
        showInfoWindow(item);
      }
    }

    // ======================================
    // 情報ウィンドウを表示
    // ======================================
    function showInfoWindow(item) {
      // 既存のPopupがあれば削除
      if (currentPopup) {
        map.removeWidget(currentPopup);
        currentPopup = null;
      }

      const postcode = item.post_code || '郵便番号不明';
      const address = item.address || '住所情報なし';
      const addressRead = item.address_read || '';
      const postLevel = item.post_level || '';
      const postType = item.post_type || '';
      const distance = item.distance ? formatDistance(item.distance) : '';

      // HTMLコンテンツを作成
      const htmlContent = `
        <div style="padding: 15px; min-width: 250px; max-width: 350px;">
          <h3 style="margin: 0 0 15px 0; font-size: 18px; color: #333; border-bottom: 2px solid #667eea; padding-bottom: 8px;">
            ${postcode}
          </h3>
          <div style="font-size: 14px; line-height: 1.6; color: #555;">
            <p style="margin: 8px 0;">
              <strong>🏠 住所:</strong><br>${address}
            </p>
            <p style="margin: 8px 0;">
              <strong>📖 読み:</strong> ${addressRead}
            </p>
            <p style="margin: 8px 0;">
              <strong>📊 レベル:</strong> ${postLevel}
            </p>
            <p style="margin: 8px 0;">
              <strong>🏷️ 種別:</strong> ${postType}
            </p>
            ${distance ? `<p style="margin: 8px 0;"><strong>📍 距離:</strong> ${distance}</p>` : ''}
          </div>
        </div>
      `;

      // Popupを作成
      const lat = item.position[1];
      const lng = item.position[0];
      currentPopup = new ZDC.Popup(
        new ZDC.LatLng(lat, lng),
        {
          htmlSource: htmlContent,
          closeButton: true,
          offset: new ZDC.Point(0, -40)
        }
      );

      // Popupをマップに追加して表示
      map.addWidget(currentPopup);
      currentPopup.show();
    }

    // ======================================
    // マーカーをクリア
    // ======================================
    function clearMarkers() {
      markers.forEach(marker => {
        map.removeWidget(marker);
      });
      markers = [];
      
      // Popupも削除
      if (currentPopup) {
        map.removeWidget(currentPopup);
        currentPopup = null;
      }
    }

    // ======================================
    // 検索結果をクリア
    // ======================================
    function clearResults() {
      clearMarkers();
      currentResults = [];
      
      const resultsList = document.getElementById('resultsList');
      const resultsCount = document.getElementById('resultsCount');
      const clearButton = document.querySelector('.clear-results');
      
      resultsList.innerHTML = '<div style="text-align: center; color: #999; padding: 20px;">検索条件を入力して検索してください</div>';
      resultsCount.textContent = '検索結果: 0件';
      clearButton.style.display = 'none';
    }

    // ======================================
    // 各タブのクリア機能
    // ======================================
    function clearDeliverySearch() {
      document.getElementById('postcodeInput').value = '';
      document.getElementById('deliveryService').value = 'standard';
      clearResults();
    }

    function clearProximitySearch() {
      document.getElementById('proximityLat').value = '35.681236';
      document.getElementById('proximityLng').value = '139.767125';
      document.getElementById('proximityRadius').value = '2000';
      document.getElementById('areaType').value = 'all';
      clearResults();
    }

    function clearAddressSearch() {
      document.getElementById('addressCodeInput').value = '';
      document.getElementById('hubName').value = '';
      document.getElementById('deliveryCapacity').value = '';
      clearResults();
    }

    // ======================================
    // ローディング表示
    // ======================================
    function showLoading() {
      const resultsList = document.getElementById('resultsList');
      resultsList.innerHTML = `
        <div class="loading">
          <div class="spinner"></div>
          <div>検索中...</div>
        </div>
      `;
    }

    function hideLoading() {
      // ローディングは検索結果で上書きされる
    }

    // ======================================
    // エラー表示
    // ======================================
    function showError(message) {
      const resultsList = document.getElementById('resultsList');
      resultsList.innerHTML = `<div class="error">${message}</div>`;
    }

    // ======================================
    // Enterキーで検索実行
    // ======================================
    document.addEventListener('keypress', function(event) {
      if (event.key === 'Enter') {
        const activeTab = document.querySelector('.tab-content.active');
        if (activeTab.id === 'delivery-tab') {
          checkDeliveryArea();
        } else if (activeTab.id === 'area-tab') {
          searchByProximity();
        } else if (activeTab.id === 'management-tab') {
          searchByAddressCode();
        }
      }
    });

コードを実行した結果は、以下になります。
postcode1.png

配送エリア判定
postcode_haisoarea.png

郵便番号を入力すると、該当地点が地図上にピン表示され、選択した配送サービスの可否が表示されます。
利用シーン例:受注確認、顧客対応でその場で配送可否を判断。

エリア調査
postcode_areachosa.png

拠点座標と調査半径を指定すると、対象範囲の郵便番号やエリア傾向(住宅/商業など)を把握できます。
利用シーン例:新規出店のエリア選定、配達網設計。

配送拠点管理
postcode_kanri.png

拠点のエリアコードを入力すると拠点位置を確認できます。複数拠点の比較や担当エリア調整に便利です。
利用シーン例:拠点の運用管理、担当割当ての可視化。

ステップ解説

Step 1:地図の初期化

APIキーは「リファラ制限」に対応しているものを使用しています。

<script src="https://test-js.zmaps-api.com/zma_loader.js?key=APIキー&auth=referer"></script>

まず、ZENRIN Maps API のローダーを読み込み、地図を初期化します。
地図生成のコールバック内では、中心座標(東京駅周辺)を指定し、ズームボタン・方位コンパス・スケールバーなどの地図コントロールを追加しています。

ZMALoader.setOnLoad(function (mapOptions, error) {
  const initialLat = 35.681236;
  const initialLng = 139.767125;
  mapOptions.center = new ZDC.LatLng(initialLat, initialLng);
  mapOptions.zoom = 15;

  map = new ZDC.Map(document.getElementById('ZMap'), mapOptions, function () {
    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'));
  });
});

この時点で地図が正常に描画されれば初期化成功です。

Step 2:タブ切り替え機能の実装

本デモでは、「配送エリア判定」「エリア調査」「配送拠点管理」の3つのタブを用意しています。
クリックしたタブだけをアクティブ表示に切り替える処理を実装します。

function switchTab(tabName) {
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
  document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));

  document.querySelector(`[onclick="switchTab('${tabName}')"]`).classList.add('active');
  document.getElementById(`${tabName}-tab`).classList.add('active');

  updateResultsMessage(tabName);
}

タブ切り替えに連動して、結果表示エリアのメッセージも更新されるようにしています。

Step 3:配送エリア判定機能の実装

郵便番号入力欄と配送サービス選択欄から、配送可能かを判定します。
入力チェック後、郵便番号検索API(https://test-web.zmaps-api.com/search/postcode)を呼び出します。

function checkDeliveryArea() {
  const postcode = document.getElementById('postcodeInput').value.trim();
  const deliveryService = document.getElementById('deliveryService').value;

  if (!postcode) {
    alert('配送先郵便番号を入力してください');
    return;
  }

  const params = new URLSearchParams({
    post_code: postcode,
    sort: 'post_code',
    limit: '0,10',
    datum: 'JGD'
  });

  const requestUrl = `${POSTCODE_SEARCH_URL}?${params.toString()}`;
  showLoading();

XMLHttpRequest を使ってAPIリクエストを送信します。

Step 4:API呼び出しとレスポンス処理

APIからのレスポンスを受け取り、結果をJSONとして解析します。

xhr.onload = function() {
  hideLoading();
  
  if (this.status === 200) {
    const response = JSON.parse(this.responseText);
    handleDeliveryResponse(response, deliveryService);
  } else {
    showError(`配送エリア判定に失敗しました (ステータス: ${this.status})`);
  }
};

APIのステータスコードに応じて、正常/エラーを切り替えています。

Step 5:配送可否の判定ロジック

レスポンスの内容を基に、配送可能かを判定します。
ここでは、配送サービス種別(標準・速達・当日)によって配送可能範囲を条件分岐させることを想定しています。

function handleDeliveryResponse(response, deliveryService) {
  const items = response.result.item || [];
  if (items.length === 0) {
    showDeliveryUnavailable('指定された郵便番号は配送対象外です');
    return;
  }

  const isDeliverable = checkDeliveryAvailability(items[0], deliveryService);
  displayResultsOnMap(items);
}

ここでは checkDeliveryAvailability() 関数で詳細なロジックを管理し、条件に応じたメッセージを出力します。

Step 6:地図上への結果表示

検索結果の住所情報を地図上にマーカーとして表示します。

function displayResultsOnMap(items) {
  clearMarkers();

  items.forEach(item => {
    const lat = item.geometry.coordinates[1];
    const lng = item.geometry.coordinates[0];
    const marker = new ZDC.Marker(new ZDC.LatLng(lat, lng));
    map.addWidget(marker);
    markers.push(marker);
  });
}

結果が複数ある場合も考慮し、既存マーカーを削除してから新しいマーカーを描画するようにしています。

Step 7:補助機能(クリア/ローディング/エラー処理)

UIとして、次の3つの補助機能を実装しています。

  • ローディング表示:検索中にスピナーを表示
  • クリアボタン:検索結果をリセット
  • エラーメッセージ:APIエラー時にユーザーへ通知
function showLoading() {
  document.getElementById('resultsList').innerHTML = `
    <div class="loading">
      <div class="spinner"></div>
      検索中です...
    </div>`;
}

function showError(message) {
  document.getElementById('resultsList').innerHTML = `
    <div class="error">${message}</div>`;
}

おわりに

今回は、ZENRIN Maps API の郵便番号検索を使って、
配送可能エリアを判定・地図表示する仕組みを実装しました。

「郵便番号から座標を取得し、地図上に結果を可視化する」
という一連の流れを通して、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?