Skip to content

Resolve place names with Places API (New) instead of the legacy Places API - #2124

Open
carlosinfantes wants to merge 3 commits into
datacommonsorg:masterfrom
carlosinfantes:migrate-places-api-new
Open

carlosinfantes wants to merge 3 commits into
datacommonsorg:masterfrom
carlosinfantes:migrate-places-api-new

Conversation

@carlosinfantes

Copy link
Copy Markdown

The place-name resolver calls FindPlaceFromText from googlemaps.github.io/maps, which is backed by the legacy Places API. Google has designated that API as Legacy — no new feature development, and the pricing FAQ states the legacy services are expected to be turned down in the coming years.

Why this matters in practice

A deployment whose project has not enabled places-backend.googleapis.com gets REQUEST_DENIED from Maps, and the resolver turns it into a 500 (codes.Internal):

failed to resolve place names: maps: REQUEST_DENIED - You are calling a legacy API,
which is not enabled for your project. To get newer features and more functionality,
switch to the Places API (New) or Routes API.

Every request naming a place absent from the KG fails that way — including through the MCP server, where the raw upstream error reaches the caller. Google’s own error text points at the fix this PR implements.

The change

The resolver only ever needed one thing: given a text query, return place ids. That maps one-to-one onto SearchText in Places API (New), so this swaps the client and narrows the interface to what the caller actually uses:

FindPlaceIDsFromText(ctx context.Context, query string) ([]string, error)

The existing comment already described MapsClient as "a thin facade ... for ease of testing", so this keeps that shape while dropping the leaked SDK types from the signature. findPlaceIDsForEntity collapses to a single delegating call.

The field mask is pinned to places.id. Text Search bills at the highest tier of any field requested, so requesting only the id keeps the call in the cheapest SKU — which also carries a larger monthly free allowance than the legacy Find Place SKU it replaces.

Narrowing the interface removes the last references to the legacy module, so googlemaps.github.io/maps is dropped from go.mod entirely.

Version pin

cloud.google.com/go/maps is pinned to v1.28.0: v1.29.0 and later require go >= 1.25, and this module is on go 1.24.0. Happy to bump it if you would rather move the toolchain.

Verification

  • go build ./... — clean
  • go vet ./internal/maps/... ./internal/server/recon/... — clean
  • go test ./internal/server/recon/... — passes (recon/golden needs Bigtable credentials and was not run)
  • FakeMapsClient contract unchanged: "Hawaii" still resolves to hawaii_place_id, anything else returns empty

Note: the Terraform in datacommonsorg/datacommons would need a matching change — enabling places.googleapis.com and updating the API key apiTargets, where the entry is currently the non-qualified string places_backend. Happy to send that separately if this direction looks right.

…s API

The place-name resolver calls FindPlaceFromText from googlemaps.github.io/maps,
which is backed by the legacy Places API. Google has designated that API as
Legacy: it receives no new feature development, and the documentation states
that the legacy services are expected to be turned down in the coming years.

Operationally this also bites at deployment time. A project that has not
enabled places-backend.googleapis.com gets REQUEST_DENIED from Maps, which
surfaces to the caller as a 500 (codes.Internal) reading "failed to resolve
place names: maps: REQUEST_DENIED - You're calling a legacy API, which is not
enabled for your project ... switch to the Places API (New) or Routes API".
Any request naming a place absent from the KG fails that way.

The resolver only ever needed one thing: given a text query, return place ids.
That maps one-to-one onto SearchText in Places API (New), so this swaps the
client and narrows the interface to what the caller actually uses:

  FindPlaceIDsFromText(ctx context.Context, query string) ([]string, error)

The field mask is pinned to "places.id". Text Search bills at the highest tier
of any field requested, so requesting only the id keeps the call in the
cheapest SKU -- which also carries a larger monthly free allowance than the
legacy Find Place SKU it replaces.

Narrowing the interface removes the last references to the legacy module, so
googlemaps.github.io/maps is dropped from go.mod entirely.

cloud.google.com/go/maps is pinned to v1.28.0: v1.29.0 and later require
Go >= 1.25, and this module is on go 1.24.0.

Verified with go build ./..., go vet and go test on the affected packages.
The FakeMapsClient contract is unchanged -- "Hawaii" still resolves to
hawaii_place_id and anything else returns empty.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the Google Maps integration from the legacy googlemaps.github.io/maps library to the official cloud.google.com/go/maps Places API client. It replaces the FindPlaceFromText method with FindPlaceIDsFromText using the new SearchText endpoint, which includes setting a narrow field mask (places.id) to optimize billing. Feedback suggests wrapping the generic error returned by SearchText with a more descriptive message and ensuring only non-empty place IDs are appended to the returned slice.

Comment thread internal/maps/maps.go Outdated
Comment on lines +47 to +55
resp, err := c.client.SearchText(ctx, &placespb.SearchTextRequest{TextQuery: query})
if err != nil {
return nil, err
}

placeIDs := make([]string, 0, len(resp.GetPlaces()))
for _, place := range resp.GetPlaces() {
placeIDs = append(placeIDs, place.GetId())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To adhere to defensive programming practices and the repository's general rules, we should:

  1. Wrap the generic error returned by SearchText with a more descriptive, user-friendly error message to aid in debugging.
  2. Ensure we only append non-empty place IDs to the returned slice, filtering out any potential nil or empty values.

Note: You may need to import the "fmt" package if it is not already imported.

Suggested change
resp, err := c.client.SearchText(ctx, &placespb.SearchTextRequest{TextQuery: query})
if err != nil {
return nil, err
}
placeIDs := make([]string, 0, len(resp.GetPlaces()))
for _, place := range resp.GetPlaces() {
placeIDs = append(placeIDs, place.GetId())
}
resp, err := c.client.SearchText(ctx, &placespb.SearchTextRequest{TextQuery: query})
if err != nil {
return nil, fmt.Errorf("failed to search text via Places API: %w", err)
}
placeIDs := make([]string, 0, len(resp.GetPlaces()))
for _, place := range resp.GetPlaces() {
if id := place.GetId(); id != "" {
placeIDs = append(placeIDs, id)
}
}
References
  1. Wrap generic errors from underlying functions with more descriptive, user-friendly error messages.

@carlosinfantes carlosinfantes Sep 30, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, both points applied in 711d5c5: the SearchText error is now wrapped with %w, and candidates without an id are skipped.

I applied them by hand rather than through the suggestion block, because the block was written against the first commit and would have dropped MaxResultCount: 1. That cap was added deliberately: findPlaceIDsForEntity only keeps placeIDs[0], and without it Text Search returns up to 20 results that are billed and then discarded.

…viour

findPlaceIDsForEntity keeps only placeIDs[0] and discards the rest. Text Search
returns up to 20 candidates by default, so without a cap the extra results are
billed, serialised and thrown away. Legacy Find Place returned a single
candidate, so MaxResultCount=1 also preserves the previous behaviour.
Adds context to errors returned by SearchText and ignores any candidate that
comes back without an id, so an empty string can never reach the resolver as
placeIDs[0].

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant