A maps tool lets an agent answer questions that depend on the physical world: which pharmacies near the hotel are open now, how long the drive to the depot takes at rush hour, whether a venue is wheelchair accessible according to people who have been there. Language models are bad at this on their own. Places open and close, opening hours change and road times depend on traffic, and a model's training data is a frozen, partial snapshot of all three. What the agent needs is a live lookup and a way to show the user where the answer came from.
ADK for Java gives you two routes. One is the built-in GoogleMapsTool, which switches on Gemini's server-side Grounding with Google Maps. The other is an ordinary FunctionTool that calls a Maps Platform API you hold a key for. This article explains what each one does in ADK Java 1.11.0, checked against the jar rather than the stub this page replaces. It covers how to pass the user's location without leaking it, how to turn on routing, how to read and display the sources, and when the function-tool route is the better engineering choice.
Two kinds of maps tool
The two routes differ in who runs the query and in what shape the result comes back.
| GoogleMapsTool (grounding) | FunctionTool over a Maps API | |
|---|---|---|
| Who calls Maps | Gemini, server side, inside one model call | Your Java code, as a normal tool call |
| What the model sees | Maps content it retrieved itself | The JSON your tool returns |
| What you get back | Answer text plus grounding metadata | Whatever fields you requested |
| Models | Gemini models that support Maps grounding | Any model that can call functions |
| Control over queries | Little: the model fans out queries | Full: you choose fields, radius and limits |
| Display obligations | Google Maps source attribution rules | The terms of the API you call |
Grounding is the faster way to good conversational answers: you write no client, and the model can combine place details and review snippets in one pass. The function tool is the better choice when the next step is a computation rather than a sentence, such as ranking twenty candidate sites by drive time, joining places against your own inventory, or recording a place ID in a booking. Many production agents end up with both.
What GoogleMapsTool does in 1.11.0
Disassembling com.google.adk.tools.GoogleMapsTool in 1.11.0 shows how small it is. It extends BaseTool with the name google_maps and exposes a shared INSTANCE. Its only behaviour is processLlmRequest, which copies the request's existing tool list and appends one Tool whose googleMaps field is GoogleMaps.builder().build(), an empty object. It does not declare a function, so the model never emits a function call for it and no tool event appears in the session. The lookup happens inside the model call.
Three consequences follow from the empty object. It sets no groundingTypes, and Google's documentation says that when they are omitted the service grounds on Places data only, so route questions are not grounded. It sets no location, so "near me" means nothing unless the prompt says where the user is. And unlike BuiltInCodeExecutionTool, it performs no model-name check, so ADK will happily send it to a model that does not support Maps grounding and you find out from the API error.
Nothing in the 1.11.0 jar rejects mixing GoogleMapsTool with function tools either; whether that works is up to the model. Google documents that Gemini 3 models accept built-in tools alongside function declarations. For older models, keep the grounded agent separate and wrap it in an AgentTool, the same workaround described for search in Google Search tool integration. Check the supported-models table in the Grounding with Google Maps documentation for the model you deploy. It changes often enough that copying it into code comments is a mistake.
Passing the user's location
Location goes in the request's tool configuration: toolConfig.retrievalConfig.latLng, with an optional languageCode. In ADK the natural place to set it is a before-model callback, because the location belongs to the user and the turn, not to the agent definition. The callback must merge into the existing config rather than replace it, or it will drop the tools list and any functionCallingConfig already present.
static Maybe<LlmResponse> attachLocation(CallbackContext ctx, LlmRequest.Builder req) {
Object lat = ctx.state().get("user:geo_lat"); // set only after consent
Object lng = ctx.state().get("user:geo_lng");
if (!(lat instanceof Number) || !(lng instanceof Number)) return Maybe.empty();
GenerateContentConfig cfg = req.build().config()
.orElseGet(() -> GenerateContentConfig.builder().build());
ToolConfig tc = cfg.toolConfig().orElseGet(() -> ToolConfig.builder().build());
RetrievalConfig rc = RetrievalConfig.builder()
.latLng(LatLng.builder()
.latitude(round(((Number) lat).doubleValue()))
.longitude(round(((Number) lng).doubleValue())))
.languageCode("en_US")
.build();
req.config(cfg.toBuilder().toolConfig(tc.toBuilder().retrievalConfig(rc).build()).build());
return Maybe.empty(); // continue to the model
}
static double round(double v) { return Math.round(v * 100.0) / 100.0; } // about 1 kmTreat coordinates as personal data. Store them only after an explicit consent step, keep them in user: state so they survive sessions but not users, and round them to the precision the question needs. Two decimal places is about a kilometre, enough for "coffee nearby" and useless for tracking someone. Keep raw coordinates out of logs and traces. A callback that runs on every model call is also a convenient place to drop the location when the user withdraws consent.
Turning on routing
To ground questions about directions and travel time you need a tool that sets routing. The SDK types support it, and BaseTool is the extension point, so a replacement takes a dozen lines. Use it instead of GoogleMapsTool, not alongside it, or the request will carry two Maps tools.
public final class GroundedMapsTool extends BaseTool {
private final GoogleMaps maps;
public GroundedMapsTool(boolean routing) {
super("google_maps", "google_maps");
GoogleMapsGroundingTypes.Builder types = GoogleMapsGroundingTypes.builder()
.places(GoogleMapsPlaces.builder().build());
if (routing) types.routing(GoogleMapsRouting.builder().build());
this.maps = GoogleMaps.builder().groundingTypes(types.build()).build();
}
@Override
public Completable processLlmRequest(LlmRequest.Builder req, ToolContext ctx) {
GenerateContentConfig cfg = req.build().config()
.orElseGet(() -> GenerateContentConfig.builder().build());
List<Tool> tools = new ArrayList<>(cfg.tools().orElse(List.of()));
tools.add(Tool.builder().googleMaps(maps).build());
req.config(cfg.toBuilder().tools(tools).build());
return Completable.complete();
}
}Google's documentation notes that search along a route needs both places and routing. Routing grounding returns distance, duration and an encoded polyline in the grounding chunks, which is enough to draw the route on your own map.
Reading the sources
Grounded answers arrive as ordinary text plus LlmResponse.groundingMetadata(). The useful parts are groundingChunks, where each chunk's maps() carries a title, a uri and a placeId (and, for routes, a route with distanceMeters, duration and encodedPolyline), and groundingSupports, which map text segments to chunk indices. ADK copies the metadata onto the event it emits, and Event.groundingMetadata() exists in 1.11.0, so the simplest client renders sources from the final response event it already receives. When a later step rewrites the answer, or you want sources in an audit trail, an after-model callback can also keep them in session state under an unprefixed key, which lasts for the session:
static Maybe<LlmResponse> keepMapSources(CallbackContext ctx, LlmResponse resp) {
resp.groundingMetadata().flatMap(GroundingMetadata::groundingChunks).ifPresent(chunks -> {
List<Map<String, String>> sources = new ArrayList<>();
for (GroundingChunk ch : chunks) {
ch.maps().ifPresent(m -> sources.add(Map.of(
"title", m.title().orElse(""),
"uri", m.uri().orElse(""),
"placeId", m.placeId().orElse(""))));
}
if (!sources.isEmpty()) ctx.state().put("map_sources", sources);
});
return Maybe.empty(); // keep the model's response
}The SDK type also has googleMapsWidgetContextToken on the metadata and enableWidget on the tool, for rendering Google's contextual Maps widget. The Vertex documentation page we checked does not describe them, so read the current docs before relying on either.
Attribution is part of the design
Grounding with Google Maps comes with display rules, and they constrain your UI. Google's documentation requires that the Google Maps sources immediately follow the content they support and that each source shows its title and links to its uri. The text "Google Maps" must not be altered, wrapped or localized, and it should carry translate="no". Voice interfaces need a companion screen that shows sources. Two engineering points follow. First, sources must flow from the model response to the client, either on the final event or through the state copy above. Second, any post-processing that rewrites the answer (a summarizer, a translation step, a formatter agent) can separate text from sources. Keep grounded answers on a path that preserves both. The same documentation states that place IDs may be stored, so persisting the placeId of a place the user chose is allowed. It also says Maps grounding currently supports English prompts and responses, which matters for an internationalized agent.
The function-tool route
When you need structured results, write the tool yourself. The pattern is the same as any custom function tool: a small, typed method with tight inputs, a client that requests only the fields it needs, and a result trimmed for the model.
public final class PlacesTools {
private final PlacesClient places; // your wrapper over Places API (New) Text Search
@Annotations.Schema(name = "search_places",
description = "Find places matching a query near a point. Returns at most 5.")
public Map<String, Object> searchPlaces(
@Annotations.Schema(name = "query") String query,
@Annotations.Schema(name = "radius_m") Integer radiusM,
ToolContext ctx) {
Double lat = (Double) ctx.state().get("user:geo_lat");
Double lng = (Double) ctx.state().get("user:geo_lng");
if (lat == null || lng == null) {
return Map.of("status", "error", "reason", "location_unknown",
"hint", "Ask the user for a city or address.");
}
int r = Math.min(radiusM == null ? 1500 : radiusM, 5000); // cap in code
List<Place> hits = places.textSearch(query, lat, lng, r, 5,
"places.id,places.displayName,places.formattedAddress,places.currentOpeningHours.openNow");
return Map.of("status", "ok", "places", hits.stream().map(Place::toModelMap).toList());
}
}The field list is the important line. Places API (New) requires a field mask, and the fields you request determine the billing tier, so asking for everything is both slow and expensive. Caps on radius and result count belong in code, not in the prompt. Errors should use a shape the model can act on, as described in wrapping tool errors. Check the Maps Platform terms before caching any response. The grounding documentation explicitly allows storing place IDs, but do not assume that covers anything else.
Worked example: a pharmacy in Lisbon
Take a travel assistant on Gemini 3 with GroundedMapsTool(true), the location callback and the source callback. The user, who shared their location at a Lisbon hotel, asks: "Is there a pharmacy open now within walking distance, and how long is the walk?"
- The before-model callback rounds the stored coordinates to 38.71, -9.14 and merges them into
toolConfig.retrievalConfig. - The model runs several Maps queries server side: a place search for pharmacies, then a walking route to the best candidate. Each query is billed.
- The response text names two pharmacies with their hours and a twelve-minute walk. Its grounding metadata holds two place chunks and one route chunk.
- The client reads the three sources from the final event's grounding metadata and renders them directly under the answer with the Google Maps attribution. The after-model callback also keeps them in
map_sourcesfor the audit log. - The user picks one. A booking tool stores its
placeIdin the itinerary, which the terms allow.
Swap the user's question for "rank our 40 warehouses by drive time from this customer" and grounding is the wrong tool: you want numbers in a table. A function tool over a routes matrix API, called once, returns the data, and the model only explains the result.
Failure modes
- Ungrounded routes. The stock tool grounds only on Places, so travel times in the answer can be the model's guesses. Use a routing-enabled tool, and test that route answers carry route chunks.
- Wrong "near me". No location is sent, and the model picks a city from context or invents one. Send
latLng, or have the agent ask. - Config clobbered. A callback that builds a fresh
GenerateContentConfigdrops the tools other components added. Always merge withtoBuilder(). - Unsupported model. ADK does not check the model name, so a fallback to an older or non-Gemini model fails at the API. Route fallbacks to an agent without the tool.
- Lost attribution. A downstream rewrite step drops the sources, and the page breaks the display rules. Render from the grounded event, or carry sources in session state past the rewrite.
- Cost surprises. One prompt can trigger several billed Maps queries. Keep a per-user budget and count grounded calls from usage metadata.
- Location leaks. Raw coordinates end up in traces or prompts in plain logs. Round them, store them only with consent, and redact them from telemetry.
Trade-offs
Grounding trades control for convenience. You get fluent, sourced answers with almost no code, but you cannot see or cap the individual queries, the result shape is prose, and you inherit both a supported-model list and display rules. The function-tool route costs a client, a field mask and quota handling, but every call is visible, cacheable within the terms, testable with fakes and portable across models. A practical split: grounding for open-ended conversational questions, function tools for anything that feeds a computation, a booking or a stored record. If you already run a web search tool, the trade-offs in the provider-neutral search tool carry over almost unchanged.
What to do next
- Confirm that your production model appears in Google's Maps grounding supported-models table, and decide what your fallback model does without the tool.
- Replace
GoogleMapsTool.INSTANCEwith a tool that setsgroundingTypesexplicitly if any user question involves travel time or directions. - Add a consent step and a before-model callback that merges a rounded
latLngintotoolConfig. - Add an after-model callback that saves map sources, and render them under the answer following the attribution rules.
- List the questions that feed computations and give those a function tool with a field mask, caps and an error envelope.
- Write ten evaluation prompts, half place and half route, and assert that each grounded answer carries the expected chunk type.
- Add a per-user daily budget for grounded calls and alert on spikes.