癒し系アプリに「いま開いている場所の近くで、気分転換に立ち寄れる静かなカフェ」を提案する機能を足せないか、と考えたことがあります。個人開発で続けているアプリの多くは位置情報を扱ってこなかったので、最初に頭をよぎったのは「世界中の店舗データをどうやって自前で持つのか」という現実的な壁でした。Places の有料 API を叩いてランキングを自作するのは、ひとりで運用するには重すぎます。
その壁を回避できるのが、Gemini API の Google Maps グラウンディングです。モデルに「場所の文脈」を渡し、回答を Google Maps の実データ(店舗・レビュー・営業時間など)に裏づけてもらう仕組みで、2026年6月時点では Gemini 3.5 Flash を含む複数モデルで使えます。ただ、動かすだけなら数行で終わる一方、課金の構造と出典表示のルールを知らずに本番へ出すと、後で痛い目を見ます。私が実装しながら整理した勘所を、順を追って残しておきます。
グラウンディングは「場所の文脈」をモデルに渡す道具
このツールの実体は、テキスト検索です。ユーザーの問いに地理的な文脈(「近くの」「San Francisco の」など)が含まれていると、モデルが Google Maps へ問い合わせて、その結果を踏まえた回答を生成します。緯度・経度を渡せば「near me(近く)」系の問いはその座標を基準に解釈され、固有名や非ローカルな問いでは座標の影響はほとんど受けません。
処理の流れは次のとおりです。ユーザーが地理的な意図を含む問いを送る、モデルがツールを呼ぶ、Maps サービスが店舗やレビューを引く、その実データで回答が組み立てられ、最後に出典つきのテキストが返ってきます。RAG を自前で組むときの「検索 → 文脈注入 → 生成」を、Google 側が場所データに特化して肩代わりしてくれる、と捉えると腑に落ちます。
注意したいのは、このツールが既定では無効だという点です。明示的に有効化したリクエストでしか動きません。これは設計ミスではなく、後述する課金とレイテンシを考えると、むしろありがたい既定値です。
最小実装 — ツールを有効化して緯度経度を渡す
まずは素直に動かします。tools に Google Maps を入れ、必要なら toolConfig で座標を渡すだけです。Python の公式 SDK(google-genai)ではこう書きます。
from google import genai
from google.genai import types
client = genai.Client() # GEMINI_API_KEY を環境変数から読む
prompt = "ここから徒歩15分以内で、静かに過ごせるカフェはありますか?"
response = client.models.generate_content(
model = "gemini-3.5-flash" ,
contents = prompt,
config = types.GenerateContentConfig(
# Google Maps グラウンディングを有効化
tools = [types.Tool( google_maps = types.GoogleMaps())],
# 任意: ユーザーの現在地を文脈として渡す(例は東京駅周辺)
tool_config = types.ToolConfig(
retrieval_config = types.RetrievalConfig(
lat_lng = types.LatLng( latitude = 35.681236 , longitude = 139.767125 )
)
),
),
)
print (response.text)
JavaScript(@google/genai)でも構造は同じで、googleMaps: {} を tools に入れ、toolConfig.retrievalConfig.latLng に座標を渡します。
import { GoogleGenAI } from "@google/genai" ;
const ai = new GoogleGenAI ({}); // GEMINI_API_KEY を環境変数から
const response = await ai.models. generateContent ({
model: "gemini-3.5-flash" ,
contents: "ここから徒歩15分以内で、静かに過ごせるカフェはありますか?" ,
config: {
tools: [{ googleMaps: {} }],
toolConfig: {
retrievalConfig: {
latLng: { latitude: 35.681236 , longitude: 139.767125 },
},
},
},
});
console. log (response.text);
ここまでで「近くのカフェ」を聞けば、それらしい回答は返ってきます。けれど、この状態のまま画面に出すと規約違反になります。返ってきた出典を、ルールに沿って必ず表示しなければならないからです。
出典の表示は任意ではなく「義務」です
回答が Maps データで裏づけられると、レスポンスに groundingMetadata が乗ります。中身は大きく2つで、groundingChunks が出典(uri・title・placeId)の配列、groundingSupports が「本文のどの範囲が、どの出典に対応するか」を startIndex・endIndex とチャンク番号で結ぶ配列です。後者があるおかげで、本文の特定の一文に対してインライン引用を組み立てられます。
公式の利用要件では、Maps グラウンディングの結果を見せるときは、対応する Google Maps の出典を「そのコンテンツの直後に」「1回の操作で見える形で」提示することが求められています。さらに表記にも細かい決まりがあり、Google Maps という文字列は改変してはいけません。大文字小文字を変えない、途中で折り返さない、他言語へ訳さない、ブラウザの自動翻訳を防ぐために translate="no" を付ける、といった具合です。
最低限の出典リストは、たとえば次のように組み立てます。
function renderSources ( response ) {
const grounding = response.candidates?.[ 0 ]?.groundingMetadata;
const chunks = grounding?.groundingChunks ?? [];
// 出典が1件もなければ Maps グラウンディングは成立していない
if (chunks. length === 0 ) return null ;
const items = chunks
. filter (( c ) => c.maps) // maps ソースだけを対象にする
. map (( c ) => ({
title: c.maps.title,
uri: c.maps.uri, // maps.google.com への参照リンク
placeId: c.maps.placeId,
}));
return items; // この配列を「Google Maps」表記つきで本文直後に描画する
}
HTML 側では、表記を改変しない一行を必ず添えます。
< p class = "gmp-attribution" translate = "no" >Google Maps</ p >
< ul >
<!-- ここに renderSources() の結果をリンクとして並べる -->
</ ul >
placeId と reviewId はキャッシュ・保存・エクスポートが許可されているので、店舗を自分のDBに紐づけて再利用できます。一方で本文の出典提示そのものを省くことはできません。「出典が0件のレスポンスは、Maps グラウンディングが成立していない=課金対象外」という後述の仕様とも符合するので、groundingChunks の有無は表示と課金判定の両方の起点になります。
料金は本体推論と別枠 — 「常時ON」が破綻する理由
ここが個人開発者にとって一番のキモです。Google Maps グラウンディングの料金は、モデルの入出力トークンとは別枠で、グラウンディングに成功したプロンプト 1,000 件あたり $25(2026年6月時点)。無料枠は1日あたり最大500リクエストです。課金は「Maps の出典を少なくとも1件含む結果が返ったプロンプト」だけが対象で、1リクエスト内で Maps へ複数回問い合わせても1件として数えられます。
数字にすると重さが分かります。仮に地理系の問い合わせが1日100件あるアプリなら、月およそ3,000件で約 $75。1日500件まで膨らめば月15,000件で約 $375 です。広告(AdMob)で薄く回している無料アプリに、全リクエストでこの単価が乗ると採算は簡単に崩れます。比較として、本体推論との単価感の違いを並べておきます。
項目 課金単位 無料枠の目安
Maps グラウンディング 成功プロンプト1,000件あたり $25 1日500リクエスト
モデル本体の推論 入出力トークン量に応じた従量 モデル側のレート上限に準拠
公式のベストプラクティスも「不要なときは切る」と明言しています。既定で無効になっているのは、この単価を踏まえれば妥当な判断です。つまり実装の主眼は「どう有効化するか」ではなく「どのリクエストで有効化しないか」に移ります。
地理的な意図のときだけツールをONにする
私が採った方針は、ツールを常時ONにせず、その問いに地理的な意図がありそうなときだけ有効化する一段のゲートを前に挟むことです。判定そのものは厳密でなくてよく、軽い前処理で十分に効きます。
import re
GEO_HINTS = [ "近く" , "周辺" , "ここから" , "near me" , "徒歩" , "駅" , "店" , "カフェ" , "レストラン" ]
def looks_geographical (text: str ) -> bool :
# 地名・距離・場所カテゴリの語が含まれるかを軽く見るだけ
return any (hint in text for hint in GEO_HINTS )
def build_config (prompt: str , lat: float | None , lng: float | None ):
if not looks_geographical(prompt):
# 地理意図がなければツールを付けない = 課金対象にしない
return types.GenerateContentConfig()
tool_config = None
if lat is not None and lng is not None :
tool_config = types.ToolConfig(
retrieval_config = types.RetrievalConfig(
lat_lng = types.LatLng( latitude = lat, longitude = lng)
)
)
return types.GenerateContentConfig(
tools = [types.Tool( google_maps = types.GoogleMaps())],
tool_config = tool_config,
)
この一段を挟むだけで、雑談やアプリの使い方の質問といった非地理的なリクエストが Maps へ流れなくなり、月額が読みやすくなります。私は地理意図を「拾いすぎる」側に倒すことを推奨します。判定漏れでツールが付かなければ、せいぜい「場所に詳しくない普通の回答」が返るだけで、ユーザー体験の劣化は小さいからです。逆に拾いすぎても、出典0件なら課金されない仕様が効きます。なお Gemini 3 系では Maps グラウンディングと関数呼び出し(自前ツール)を併用できるので、座標取得や予約処理を関数として足す設計にも無理なく広げられます。
個人開発で実際に踏みやすい落とし穴
実装中に引っかかった、あるいは要件を読んで「これは事故るな」と感じた点を挙げておきます。
第一に、座標を渡し忘れたまま「near me(近く)」を投げる失敗です。緯度・経度がないと、モデルは何を基準にすればよいか分からず、期待した近接結果になりません。クライアントの位置情報許可が取れていないときは、地理意図と分かっていても「現在地が必要です」と素直に促すほうが、的外れな回答を返すより誠実だと感じます。
第二に、提供できない地域の問題です。Maps グラウンディングを使うアプリを「禁止地域」で配布・宣伝することは規約で制限されています。多言語で世界中に配信している個人開発アプリほど、配信地域の確認は早めに済ませておくほうが安全です。
第三に、入出力はテキストに限られる点です。現時点では Maps グラウンディングはマルチモーダルの入出力に対応していません。写真から店舗を当てる、といった用途は別の仕組みと組み合わせる必要があります。
第四に、レイテンシです。会話型UIに組み込むなら、グラウンディングありの応答の P95 を計測し、許容範囲に収まっているか監視することが勧められています。Maps への問い合わせが1往復挟まる分、素のモデル応答より遅くなる前提で、ローディング表現を用意しておくと体験が安定します。
次の一歩
まずは手元のアプリで、地理意図のありそうな問いを5つほど書き出し、座標つき・座標なしの両方で groundingChunks が返るかを確認してみてください。出典が安定して返る問いの形が見えてくると、ゲートのキーワードと、出典表示UIの最小要件が同時に固まります。私自身、最初に「出典0件のときに何を見せるか」を決めてから実装に入ったことで、課金と表示の両方の判断がぶれずに済みました。同じように位置情報機能を検討している個人開発の方の、最初の地図になればうれしいです。