The Ruby client the agentic search calls into on the backend, and the React handler that enriches a selected place with full address details on the frontend.
# app/services/google_places/client.rb
# lines 120–163 (text_search_near)
def text_search_near(query:, lat:, lng:, radius_metres: 8047, type: nil, max_results: 10)
q = query.to_s.strip
return [] if q.blank?
uri = URI(TEXT_SEARCH_URL)
params = {
key: @api_key,
query: q,
location: "#{lat},#{lng}",
radius: radius_metres.to_i,
type: type.presence
}.compact
uri.query = URI.encode_www_form(params)
json = get_json_hash(uri, context: "textsearch_near")
google_status = json["status"].to_s
if google_status != "OK" && google_status != "ZERO_RESULTS"
msg = json["error_message"].presence || "Google Places text search failed"
raise Error.new(msg, http_code: 502, google_status: google_status)
end
results = Array(json["results"]).first(max_results.to_i)
results.map do |place|
next unless place.is_a?(Hash)
geometry = place["geometry"].is_a?(Hash) ? place["geometry"] : {}
location = geometry["location"].is_a?(Hash) ? geometry["location"] : {}
photo0 = Array(place["photos"]).first
primary_ref = photo0.is_a?(Hash) ? photo0["photo_reference"] : nil
{
place_id: place["place_id"],
name: place["name"],
formatted_address: place["formatted_address"],
lat: location["lat"],
lng: location["lng"],
types: Array(place["types"]),
rating: place["rating"],
primary_photo_reference: primary_ref
}
end.compact
end
Text Search (not Nearby Search) gives better semantic matching for intent-based criteria like "jazz cafe with indoor seating."
# src/components/forms/events/EventForm.tsx
# lines 718–782 (handleSelectSuggestion)
const handleSelectSuggestion = useCallback(
(suggestion: LocationSuggestion) => {
setSelectedSuggestion(suggestion);
setPlacePhotos([]);
setSearchError(null);
setPhotoIndex(0);
// Google Places Nearby Search returns addresses like:
// "123 Main St, Denver, CO 80203, USA"
const addr = parseFormattedAddress(suggestion.formatted_address);
// Auto-populate Step 6 location fields from the selected place, falling
// back to what the user typed in Step 3 if parsing comes up short.
setLocation((prev) => ({
...prev,
name: suggestion.name,
type: inferLocationType(suggestion.types) ?? prev.type,
placeId: suggestion.place_id,
streetNumber: addr.streetNumber,
streetName: addr.streetName,
city: addr.city || locationCity || prev.city,
stateProvince: addr.stateProvince || locationStateProvince || prev.stateProvince,
country: addr.country || locationCountry || prev.country,
}));
// ── Address enrichment via Places Details API ──────────────────────
// Nearby Search's "vicinity" field is abbreviated and typically omits
// postal_code, short-form state/country. Fetch full address_components
// for the selected place_id and merge in anything missing.
fetchPlaceAddress(suggestion.place_id)
.then((placeAddr: PlaceAddress) => {
setLocation((prev) => ({
...prev,
streetNumber: placeAddr.street_number || prev.streetNumber,
stateProvince: placeAddr.state_province_short || placeAddr.state_province || prev.stateProvince,
country: placeAddr.country_short || placeAddr.country || prev.country,
postalCode: placeAddr.postal_code || prev.postalCode,
}));
})
.catch(() => {
// Address enrichment is best-effort — silently ignore failures.
});
setCurrentStep(5);
},
[locationCity, locationStateProvince, locationCountry]
);
Optimistic fill first from the abbreviated search result, then a background Places Details call backfills the fields the abbreviated form leaves out.