MENU

問い合わせ


    【NuxtとGoogle Mapsで作る情報集約マップ 第7回】施設名で検索して地点を取り込む(Places API (New))

    Google Maps API とNuxtで社内向けの情報集約マップを作る連載の第7回です。前回(第6回)では、住所を Geocoding API v4 で緯度経度に変換し、その位置に地点を登録できるようにしました。今回は、「東京駅」「◯◯ビル」のような施設名で検索し、候補から選んだ施設の位置に地点を登録する機能を作ります。使うのは Google の Places API (New) と、その入力欄をまるごと提供する PlaceAutocompleteElement です。

    「施設名で検索できると便利そう。でも、Google の施設情報をたくさん取ると料金が高くなったりしない?」

    結論から言うと、そのとおりです。Places API (New) は、取得を要求したデータ項目(フィールド)の中で最も高い料金区分で課金される仕組みです。「念のため全部取る」と、1回の選択が最も高い区分になりえます。この回では、必要な項目を「位置」「住所」「帰属表示」の3つに絞り、施設名は料金区分が上がる項目を使わずに候補の文字列から取る、という工夫をしています。

    目次

    この回で作るもの:施設名検索から地点を登録

    操作画面の動き
    画面上部の「施設名で探す」に文字を入力Google の候補一覧が入力欄の下に出る(入力中に自動で候補を更新)
    候補を選ぶ施設の位置と住所を取得し、地図が移動して仮ピンが立つ
    フォームで名称(社内での呼び名)・カテゴリを入れて保存座標と一緒に place_id と「施設検索由来」の印が保存される

    前回の住所検索は画面に残したまま、施設名検索を上に追加しました。住所が分かっているなら住所検索、名前しか分からないなら施設名検索、と使い分けられます。

    仕組み:オートコンプリートと Place Details

    施設名検索は、2種類の問い合わせの組み合わせで動きます。

    段階問い合わせ何が起きるか
    入力中Autocomplete (New)入力された文字から施設の候補(名前・場所の説明・place_id)を返す
    候補を選んだ後Place Details (New)選んだ施設について、要求したフィールド(位置・住所など)を返す

    PlaceAutocompleteElement は、この入力欄・候補一覧・キーボード操作を持った部品(Web コンポーネント)です。公式の型定義の説明によると、内部でセッショントークン(入力から選択までの一連の操作を1つにまとめる印)を自動で使い、候補から作った Place の最初の fetchFields() にも同じトークンを自動で付けます。

    📰 出典:Google Maps Platform「Place Autocomplete Widget」

    なお、従来の PlacesService などの旧 Places API は、2025年3月1日以降に新しく使い始める顧客は利用できません。これから作るなら Places API (New) 一択です。

    📰 出典:Google Maps Platform「Migrate to the new Places API」

    どのフィールドがどの料金区分か

    公式ドキュメントでは、Place Details (New) のフィールドが料金区分(SKU)ごとに一覧になっています。この回に関係するものを抜き出すと次のとおりです(2026年9月時点)。

    区分主なフィールド
    Essentials IDs Onlyid、attributions、photos など
    Essentialslocation、formattedAddress、addressComponents、viewport など
    ProdisplayName(施設名)、businessStatus、googleMapsUri、primaryType など
    Enterpriserating、regularOpeningHours、websiteUri、電話番号 など
    Enterprise + Atmospherereviews、editorialSummary など

    📰 出典:Google Maps Platform「Place Details (New)」

    注意したいのは、施設名(displayName)が Pro 区分であることです。そこでこのサンプルでは displayName を要求せず、候補一覧に表示されている名前(PlacePrediction の mainText)を使います。要求するのは location・formattedAddress・attributions の3つなので、Place Details は Essentials 区分になります。

    セッションの課金も区分で変わります。公式の Session pricing によると、Essentials のフィールドで終わるセッションでは、オートコンプリートの最初の12回までは1回ごとに課金され、13回目以降は無料、最後の Place Details が Essentials として課金されます。Pro 以上のフィールドで終わるセッションでは、オートコンプリート分は無料になる代わりに、最後の Place Details は要求フィールドにかかわらず Enterprise + Atmosphere の区分で課金されると説明されています。

    📰 出典:Google Maps Platform「Session pricing(Maps JavaScript API)」

    どちらが安いかは、利用者が候補を選ぶまでに何文字打つか(何回問い合わせるか)と単価によって変わります。単価は改定されるため、公式の料金ページで確認してください。

    実装

    ファイル役割
    app/composables/usePlaces.ts入力欄の作成と、選んだ施設の情報取得(新規)
    app/components/PlaceSearch.vue施設名の検索欄(新規)
    app/composables/useDraftPoint.ts / PointForm.vue出どころに帰属表示を追加(更新)
    app/pages/index.vue検索欄の配置と、選択時に仮ピンを立てる処理(更新)

    サーバー側の変更はありません。前回、POST /api/points が source(places を含む)と placeId を受け付け、Google 由来なら取得日時を記録するようにしてあるためです。

    手順1:Cloud コンソールで Places API (New) を有効にする

    ブラウザ用キーの「API制限」に Places API (New) を追加します。旧「Places API」ではない点に注意してください。オートコンプリートは Maps JavaScript API と同じブラウザ用キーで動くので、キーの HTTPリファラー制限はそのまま効きます。

    手順2:必要なフィールドだけ取る composable

    app/composables/usePlaces.ts(抜粋)

    // 取得するフィールド。料金は「要求したフィールドのうち最も高い区分」で決まる。
    //   location / formattedAddress … Place Details Essentials
    //   attributions               … Essentials IDs Only
    // displayName(施設名)は Pro 区分になるので要求しない。名前は候補(PlacePrediction)の文字列を使う。
    export const PLACE_FIELDS = ['location', 'formattedAddress', 'attributions']
    
    export function usePlaces() {
      const { load } = useGoogleMaps()
    
      async function createAutocomplete(placeholder: string) {
        const { PlaceAutocompleteElement } = await load('places')
        const el = new PlaceAutocompleteElement({
          includedRegionCodes: ['jp'], // 日本国内に限定
        })
        el.placeholder = placeholder
        return el
      }
    
      async function fetchPickedPlace(
        prediction: google.maps.places.PlacePrediction,
      ): Promise<PickedPlace | null> {
        const place = prediction.toPlace()
        // 最初の fetchFields() には、オートコンプリートのセッショントークンが自動で付く
        await place.fetchFields({ fields: PLACE_FIELDS })
        if (!place.location) return null
    
        return {
          placeId: place.id,
          name: prediction.mainText?.text ?? prediction.text.text,
          address: place.formattedAddress ?? '',
          ...toLatLngLiteral(place.location),
          attributions: (place.attributions ?? [])
            .filter((a) => a.provider)
            .map((a) => ({ provider: a.provider ?? '', uri: a.providerURI })),
        }
      }
    
      return { createAutocomplete, fetchPickedPlace }
    }

    要求フィールドを定数(PLACE_FIELDS)にまとめ、区分をコメントで残しています。後から「営業時間も出したい」と誰かが1項目足すと、それだけで区分が Enterprise に上がります。フィールドを足すことは料金の変更でもあるので、変更がレビューで目に留まるようにしておくのが狙いです。

    言語と地域は、第1回の setOptions() で language: 'ja'・region: 'JP' を指定済みです。ここでは includedRegionCodes で候補を日本国内に絞っています。第5回で作った toLatLngLiteral は、Place.location(LatLng)にもそのまま使えます。

    手順3:検索欄のコンポーネント

    app/components/PlaceSearch.vue(スクリプトの要点)

    onMounted(async () => {
      if (!isConfigured.value || !container.value) return
      try {
        autocomplete = await createAutocomplete('施設名で探す(例: 東京駅)')
        autocomplete.addEventListener('gmp-select', async ({ placePrediction }) => {
          errorMessage.value = null
          try {
            const picked = await fetchPickedPlace(placePrediction)
            if (picked) emit('pick', picked)
            else errorMessage.value = 'この施設の位置を取得できませんでした。'
          } catch {
            errorMessage.value = '施設情報の取得に失敗しました。'
          }
        })
        autocomplete.addEventListener('gmp-error', () => {
          errorMessage.value = '施設検索でエラーが発生しました(APIキーの設定を確認してください)。'
        })
        container.value.append(autocomplete)
        watchMapBounds(props.map)
      } catch (e) {
        errorMessage.value = e instanceof Error ? e.message : String(e)
      }
    })
    
    // 地図の表示範囲の近くを優先して候補を出す(範囲外も候補には残る=bias)
    function watchMapBounds(map: google.maps.Map | null) {
      idleListener?.remove()
      idleListener = null
      if (!map || !autocomplete) return
      idleListener = map.addListener('idle', () => {
        if (autocomplete) autocomplete.locationBias = map.getBounds() ?? null
      })
    }
    watch(() => props.map, watchMapBounds)

    候補の選択は gmp-select イベントで受け取り、イベントの placePrediction から toPlace() で Place を作ります。この流れは公式ガイドのサンプルと同じです。

    locationBias には地図の表示範囲を入れています。公式ガイドでは、範囲を指定しないと利用者のIPアドレスから推定した場所の近くが優先され、人によって候補が変わりうるため、できるだけ範囲を指定するよう勧めています。「bias(優先)」なので範囲外の施設も候補から消えることはありません。範囲内だけに絞りたい場合は locationRestriction を使います。

    PlaceAutocompleteElement はブラウザでしか動かないので、ページ側では <ClientOnly> の中に置きます。

    手順4:選んだ施設に仮ピンを立てる

    app/pages/index.vue(追加部分)

    // 施設検索で選んだ施設 → 同じく仮ピンを立てる。名称欄には自動で入れない(本文参照)
    function onPlacePick(p: PickedPlace) {
      if (!map.value) return
      clearSelection()
      setDraft(map.value, { lat: p.lat, lng: p.lng }, {
        source: 'places',
        placeId: p.placeId,
        label: p.address ? `${p.name}(${p.address})` : p.name,
        attributions: p.attributions,
      })
      map.value.panTo({ lat: p.lat, lng: p.lng })
      map.value.setZoom(17)
    }

    前回の住所検索と同じく、useDraftPoint の「出どころ」に source: 'places' と place_id を渡します。登録フォームはこれを POST /api/points にそのまま送るので、フォーム側の変更は表示だけです。

    app/components/PointForm.vue(テンプレートの追加部分)

        <div v-if="origin" class="point-form__origin">
          <p>{{ origin.source === 'places' ? '施設検索の結果' : '住所検索の結果' }}: {{ origin.label }}</p>
          <!-- 第7回: Places の結果に帰属表示があれば出す -->
          <p v-for="a in origin.attributions ?? []" :key="a.provider" class="point-form__attribution">
            提供:
            <a v-if="a.uri" :href="a.uri" target="_blank" rel="noopener">{{ a.provider }}</a>
            <template v-else>{{ a.provider }}</template>
          </p>
        </div>

    Places の結果には、第三者の提供元の帰属表示(attributions)が付くことがあり、表示が求められます。取得した場合はフォームに「提供: ◯◯」として出します。

    施設名を名称欄に自動で入れない理由

    候補の施設名は画面に表示するだけで、フォームの「名称」欄には自動で入れていません。利用規約(Service Specific Terms)では、Places API 由来の緯度・経度は連続30暦日まで一時保存でき、その後は削除すること、place_id は保存の制限の例外であることが定められています。一方、施設名や住所は例外として挙がっていないため、サンプルでは Google の文字列を自動で DB に入れないようにしました。名称欄には「◯◯支店」のような社内での呼び名を入れてもらう運用です。

    📰 出典:Google Maps Platform Service Specific Terms

    📰 出典:Google Maps Platform「Places API Policies」

    座標は前回と同じく、source・fetched_at と一緒に保存し、30日を超えたものは再取得または削除する運用が必要です(再取得の仕組みは本連載では未実装)。ここまでの扱いは筆者の読み方にもとづく設計例なので、実際の運用可否は法務部門や専門家に確認してください。

    動作確認の方法

    1. Cloud コンソールでブラウザ用キーの API制限に Places API (New) を追加し、npm run dev
    2. 「施設名で探す」に「東京駅」と入力 → 候補が日本語で出ることを確認
    3. 候補を選ぶ → 地図が移動し、仮ピンとフォームに「施設検索の結果」が出ることを確認
    4. 名称を入れて保存 → curl localhost:3000/api/points で "source":"places"、placeId、fetchedAt が入っていることを確認
    5. ブラウザの開発者ツールの Network タブで、候補選択後の Place Details の通信に含まれる要求フィールドが location・formattedAddress・attributions だけであることを確認

    筆者の環境では、npm run build と npm run typecheck の成功、ビルド後のサーバーが返すHTMLに検索欄の領域が含まれ、ページが正常に表示されること、source: "places" と placeId 付きの登録で取得日時が記録されることを curl で確認しました。有効なキーが無いため、候補の表示(日本語表記を含む)、施設の選択、Place Details の通信内容、帰属表示は確認できていません。 実装は公式ガイドと型定義(@types/google.maps 3系)にもとづいています。ご自身のキーで確認してください。

    つまずきやすい点・セキュリティ上の注意

    • 旧 Places API を有効にしてしまう:Cloud コンソールで有効にするのは「Places API (New)」です
    • fetchFields に項目を足しすぎる:1項目で区分が上がります。displayName も Pro 区分です
    • fetchFields を何度も呼ぶ:セッショントークンが自動で付くのは最初の呼び出しだけです。同じ施設の情報は1回で取り切ります
    • 候補の表示範囲を指定しない:利用者ごとに候補が変わりえます。地図の範囲などで locationBias を指定します
    • Google のロゴや帰属表示を消す:候補一覧の Google ロゴや、結果の帰属表示は CSS で隠さないでください

    発注者向けメモ

    • Places API (New) は「どの項目を取るか」で料金区分が決まる構造です。「施設の電話番号や営業時間も一緒に保存したい」という要望は、1回あたりの区分を大きく上げます。要件を出すときは、本当に必要な項目かを一つずつ確認しましょう
    • 施設名で検索する機能と、住所で探す機能はコスト構造が違います。住所検索(第6回)はボタンを押した回数、施設名検索は入力中の候補表示と最後の詳細取得の組み合わせで課金されます
    • Google の施設情報をそのまま台帳に保存してよいかは、規約上の確認事項です。place_id と座標以外の保存(施設名・住所・営業時間など)を前提にした要件は、法務確認を先に済ませてください
    • 旧 Places API で作られた既存システムを改修する場合、新 API への移行が必要になる可能性があります。見積りでは移行作業が別項目になっているか確認しましょう

    発注者がやることのチェックリストです。

    • ☐ 施設検索で取得する項目(位置・住所だけか、それ以上か)を決めた
    • ☐ ブラウザ用キーの API制限に Places API (New) を追加し、クォータ上限を設定した
    • ☐ Google 由来の施設情報の保存範囲について、法務の確認を取った

    開発会社への確認に使える質問例です。

    • 「施設検索で取得している項目と、その料金区分を一覧で教えてください」
    • 「施設名を取得するために、より高い料金区分の項目を使っていませんか?」
    • 「Google から取得した施設情報のうち、DB に保存しているものは何ですか?保存期間はどう管理していますか?」

    まとめと次回予告

    この回では、PlaceAutocompleteElement で施設名を検索し、選んだ施設の位置に地点を登録できるようにしました。要求フィールドを必要最小限に絞る(施設名は候補の文字列を使う)、セッションはウィジェットに任せる、Google 由来の情報は place_id と座標だけを出どころ付きで保存する、の3点が押さえどころです。

    次回は「カテゴリで絞り込み、一覧と地図を連動させる」です。カテゴリのチェックボックスで表示する地点を絞り込み、左側に地点の一覧を出して、一覧と地図のどちらから選んでも同じ地点が強調されるようにします。

    この連載の記事一覧

    この記事は連載「NuxtとGoogle Mapsで作る情報集約マップ」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      目次