Grounding with Google Maps usually feels great in a demo. The trouble starts a few days after launch, once you begin reading the logs. Responses come back, but some of them have no map data mixed in; the bill drifts from your estimate; and users report that a restaurant's hours were wrong. None of these throw an error — they all happen quietly, with an HTTP 200.
This piece walks through the four issues that actually bite once a restaurant-search or local-info assistant is live, along with the operations-side code to handle them. The weight is less on setup and more on catching the state where things look like they're working but have silently come off the rails. It assumes a mid-to-senior engineer who has already run Maps Grounding once through Vertex AI.
A reminder before the code: Maps Grounding only works through Vertex AI, not the standard API-key Gemini API. Supported models and pricing details change quickly, so confirm the current support matrix in the Vertex AI generative AI pricing page and the official tool docs before you build. The code below keeps the supported model as an injected setting so it stays easy to swap.
Detect the "silent miss" when grounding doesn't fire
The first thing to build is a check for whether map data was actually used. Gemini only consults Maps when it decides the query is a location question. When that judgment is wrong, the model answers from its own internal knowledge and hands back plausible-sounding place names. This is the scariest failure: the response is fluent, but nothing was verified.
Base the decision on grounding_metadata, not on the response text. If there isn't a single chunk, treat the answer as not grounded in the map.
# grounding_guard.py
from dataclasses import dataclass
@dataclass
class GroundedResult:
text: str
sources: list[dict]
grounded: bool # at least one map source attached
used_maps: bool # a billable Maps-grounded response
def inspect(response) -> GroundedResult:
"""Decide whether map grounding actually fired on a response."""
sources: list[dict] = []
candidate = (response.candidates or [None])[0]
metadata = getattr(candidate, "grounding_metadata", None) if candidate else None
for chunk in getattr(metadata, "grounding_chunks", []) or []:
web = getattr(chunk, "web", None)
if web and getattr(web, "uri", None):
sources.append({
"title": getattr(web, "title", "") or "(untitled)",
"uri": web.uri,
"place_id": getattr(web, "place_id", None),
})
grounded = len(sources) > 0
return GroundedResult(
text=response.text or "",
sources=sources,
grounded=grounded,
used_maps=grounded,
)When you detect a miss, the key is to not pass it through silently. Either return the answer with a note that it isn't backed by location data, or — depending on the use case — fall back to a plain proximity lookup via the Places API. In the app I run, complaints of the "it recommended a place that doesn't exist" variety nearly disappeared once I made this note mandatory before anything reaches the user. Adding a caveat builds more trust than letting people believe a fluent wrong answer.
def to_user_payload(result: GroundedResult) -> dict:
if result.grounded:
return {"answer": result.text, "sources": result.sources, "verified": True}
# Miss: make the lack of map backing explicit
note = ("(Note: this answer wasn't confirmed against live map data. "
"Please re-check hours with each venue directly.)")
return {"answer": f"{result.text}\n\n{note}", "sources": [], "verified": False}Misses also depend on how the query is phrased. A concrete question that includes a place name, a venue, or a proximity word like "near me" tends to trigger map lookups more reliably than something abstract like "find me a cafe." Nudging the system prompt to "always anchor a location question to a place name or the current location before searching" lowers the miss rate somewhat. Just don't assume it reaches zero — keep the detection layer in place.
Attribution only counts once it's rendered
If you use Maps Grounding, displaying attribution for the sources you referenced isn't optional — it's a requirement. Pulling the data out of grounding_metadata isn't enough; it's satisfied only when it's actually drawn on the screen the user sees. The most common production slip is extracting it correctly and then dropping it on the UI side.
# attribution.py
import html
def render_attribution(sources: list[dict]) -> str:
"""Escape Maps sources safely and turn them into display HTML."""
if not sources:
return ""
items = "\n".join(
f' <li><a href="{html.escape(s["uri"])}" target="_blank" '
f'rel="noopener noreferrer">{html.escape(s["title"])}</a></li>'
for s in sources
)
return (
'<div class="maps-attribution" '
'style="font-size:13px;color:#5f6368;margin-top:12px;">\n'
" <span>Source (Google Maps):</span>\n"
f" <ul style=\"margin:4px 0;padding-left:16px;\">\n{items}\n </ul>\n"
"</div>"
)Both title and uri are externally sourced strings, so escape them before embedding. Passing them raw lets a stray character in a venue name break the layout — or, in the worst case, opens a script-injection path.
One more implementation choice: in a conversational UI, decide whether to surface the interactive map widget or just text links. If you want users to touch a map in a chat flow, enable the widget integration; for a batch process that only returns a summary server-side, text attribution is enough. The exact scope of the requirement can change, so confirm widget handling and attribution styling in the official docs each time. The point that doesn't change: once you extract the metadata, wire it through to rendering on a single, unbroken path.