Google Places Text Search
If you are building a mapping application, you need to know about the Google Maps Text Search API, which allows you to search for places using flexible, free-form text queries. This could be an exact address like “555 West Hastings Street, Vancouver” or even something more general like “best dim sum in Richmond”. The API provides detailed information on places that match your query, making it easy to access specific location data tailored to your app's needs.
In this final blog post in my Google Places API tutorial series, I'll show you how to use the Text Search API. We’ll explore worked examples, cover pricing, and build an LLM inspired query engine powered by Google Text Search (demo / source code) to help you easily find detailed information about any location in the Google Maps database.
Part 1: Finding the right place with the Google Places API
Part 2: Google address autocomplete with the Places API
Part 3: Google Place Details and Place Photos API
Part 4: Google Nearby Search API
Part 5: Google Places Text Search (this article)
What is the Google Places Text Search API?
The Google Places Text Search API, often called Google Text Search, retrieves information about a place based on its name or address without requiring a place_id
. This differs from the Google Place Details API, which specifically needs a place_id
to work.
Google Places API Text Search example
Similar to other Google Maps Platform APIs, Google Text Search works directly on https://maps.google.com/. To try it out, type in an address like "555 West Hastings Street, Vancouver" without selecting any of the Places Autocomplete suggestions, and hit the return key.
You'll immediately see details about the place associated with that address displayed on the screen. Making the same query using the Text Search API is just as easy.
Method: GET
https://maps.googleapis.com/maps/api/place/textsearch/json
?query={query}&
type={type}&
key={YOUR_API_KEY}
{query} is the text string you want to search for e.g. "555 West Hastings Street, Vancouver".
{type} is the place type that you can use to restrict search results by. For instance, if you only want to search for restaurants, you can set {type} to type=restaurant
. Only one type may be specified. If more than one type is provided, all types following the first entry are ignored.
{YOUR_API_KEY} is a valid Google Maps API key with the Places API enabled.
The Google Text Search API (see documentation) also includes radius
and region
parameters to help narrow down search results. However, these are often less necessary, as you can simply include location context within the search text itself. For instance, a query like "ramen restaurants in Tokyo" will automatically focus results on ramen spots in Tokyo, Japan, without needing additional parameters.
Here's what an API call to Text Search for “555 West Hastings Street, Vancouver” looks like:
Method: GET
https://maps.googleapis.com/maps/api/place/textsearch/json?query=555%20west%20hastings%20street%20vancouver%20bc&key=YOUR_API_KEY
Response
{
"html_attributions": [],
"results": [
{
"formatted_address": "555 W Hastings St, Vancouver, BC V6B 4N4, Canada",
"geometry": {
"location": {
"lat": 49.2846966,
"lng": -123.1119349
},
"viewport": {
"northeast": {
"lat": 49.28592632989272,
"lng": -123.1108964701073
},
"southwest": {
"lat": 49.28322667010728,
"lng": -123.1135961298927
}
}
},
"icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/geocode-71.png",
"icon_background_color": "#7B9EB0",
"icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet",
"name": "555 W Hastings St",
"photos": [
{
"height": 4624,
"html_attributions": [
"<a href=\"https://maps.google.com/maps/contrib/112068941878962933054\">Pushpinder</a>"
],
"photo_reference": "AdCG2DM2GU-DiP2gcwzIQG7Qord6xnWFqrsP_4n0DmoLeoyU29Ni4bQppCQYrp1P1NZ_Jh3vXxv2EVO6YbG361l92pLhwKXRtG-v3PLGI1JPfbuHvw3rNJeL6njBCDVQcvVuZR_LkW0741GspTIX3srCWcu225YCXWMKJ3yudPI9vE7v8cZl",
"width": 3472
}
],
"place_id": "ChIJUcKFZ3hxhlQREJGVU1foPaE",
"reference": "ChIJUcKFZ3hxhlQREJGVU1foPaE",
"types": [
"premise"
]
}
],
"status": "OK"
}
In the results
array of the response, each place
object has a:
name
, the human-readable name for the returned place
.
formatted_address
, a human-readable, properly formatted address of the place
.
geometry.location
, which gives us its lat
(latitude) and lng
(longitude).
photos.photo_reference
, a unique identifier that represents a specific photo associated with a place. In a Google Text Search API response, this identifier always corresponds to the top-ranked photo for that place
on Google Maps.
place_id
, which is its unique place ID. It can be used to retrieve the place's name, address, photos, reviews etc using the Place Details API.
Google Places API Text Search "place at" trick
To retrieve business listing information in the same Text Search API call, prepend "place at" to the address, like this: "place at 555 West Hastings Street, Vancouver".
Method: GET
https://maps.googleapis.com/maps/api/place/textsearch/json?query=place%20at%20555%20west%20hastings%20street%20vancouver%20bc&
key=YOUR_API_KEY
Response
{
"html_attributions": [],
"results": [
{
"business_status": "OPERATIONAL",
"formatted_address": "555 W Hastings St, Vancouver, BC V6B 4N6, Canada",
"geometry": {
"location": {
"lat": 49.2845838,
"lng": -123.1121818
},
"viewport": {
"northeast": {
"lat": 49.28586082989273,
"lng": -123.1110061201073
},
"southwest": {
"lat": 49.28316117010728,
"lng": -123.1137057798927
}
}
},
"icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/restaurant-71.png",
"icon_background_color": "#FF9E67",
"icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/restaurant_pinlet",
"name": "Top Of Vancouver Revolving Restaurant",
"opening_hours": {
"open_now": true
},
"photos": [
{
"height": 2268,
"html_attributions": [
"<a href=\"https://maps.google.com/maps/contrib/112618640134446180937\">A Google User</a>"
],
"photo_reference": "AdCG2DPmrhrvMSTAH_uFvmXj-vKEyRD30G-wIEiZEbS7IJ9P2OHW0gngpDWY4GoA8X9J_ANtF6k0m4xb3vjEgWMCzmCJvsaCMTcLTMPjTBqjYsg1x-3OD-vMTWrHMtOUrEkMNBh8YDxcYSi3W4Dn5P9srFyPVvmrwAClOQvNAO6iEuoxY8E",
"width": 4032
}
],
"place_id": "ChIJfe2sYHhxhlQRYY79aUl69vw",
"plus_code": {
"compound_code": "7VMQ+R4 Vancouver, British Columbia, Canada",
"global_code": "84XR7VMQ+R4"
},
"rating": 4.1,
"reference": "ChIJfe2sYHhxhlQRYY79aUl69vw",
"types": [
"restaurant",
"food",
"point_of_interest",
"establishment"
],
"user_ratings_total": 3046
},
{
"business_status": "OPERATIONAL",
"formatted_address": "HARBOUR CENTRE, 555 W Hastings St L1, Vancouver, BC V6B 4N6, Canada",
"geometry": {
"location": {
"lat": 49.2849831,
"lng": -123.1115944
},
"viewport": {
"northeast": {
"lat": 49.28639587989272,
"lng": -123.1102140701073
},
"southwest": {
"lat": 49.28369622010727,
"lng": -123.1129137298927
}
}
},
"icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/shopping-71.png",
"icon_background_color": "#4B96F3",
"icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/shopping_pinlet",
"name": "BCLIQUOR Harbour Centre",
"opening_hours": {
"open_now": true
},
"photos": [
{
"height": 3024,
"html_attributions": [
"<a href=\"https://maps.google.com/maps/contrib/114288042566495812069\">Kevin T</a>"
],
"photo_reference": "AdCG2DMqgNZXaqSTV_U-rusLEwZyASb_e5uS8e0-YSziEbuilJC3HsAgIC81Dh1lkTmTdym1g8gsDpbCaExfCbCwZ62Zq0-nKbYwElNkRvAS0J5hehLLDjQ0NNH9KgsomEHs4vKMt2UoXoYvCS3z7QqiDzFzq5xyZ0a69lGpSl3A_bDC_fjy",
"width": 4032
}
],
"place_id": "ChIJfe2sYHhxhlQRqFq5z44TMvM",
"plus_code": {
"compound_code": "7VMQ+X9 Vancouver, British Columbia, Canada",
"global_code": "84XR7VMQ+X9"
},
"rating": 4.1,
"reference": "ChIJfe2sYHhxhlQRqFq5z44TMvM",
"types": [
"liquor_store",
"store",
"point_of_interest",
"establishment"
],
"user_ratings_total": 620
},
// 18 more entries
],
"status": "OK"
}
Since the Google Text Search API results
now include business listings from a restaurant
, liquor_store
and cafe
, we get additional metadata we can display in our app:
rating
is the average rating based on aggregated user reviews.
user_ratings_total
represents the total number of reviews for the place
.
photo_reference
is a unique identifier for the primary photo linked to a place. You can use it with the Place Photos API to retrieve the image.
Google Text Search free-form text example
The Text Search API also allows you to use free-form text in your query. You can now ask any kind of complex question about places to go to, or things to do, and expect Google Maps to help you with that. For example, the search string, "Best ramen in Vancouver" searches for listings that have "ramen" mentioned in the place name, description or reviews, and sets a location bias to Vancouver, BC in Canada.
Method: GET
https://maps.googleapis.com/maps/api/place/textsearch/json?
query=best%20ramen%20in%20vancouver&
key=YOUR_API_KEY
Response
- Jinya Ramen Bar (4.4 ⭐️)
- Ramen Danbo Robson (4.6 ⭐️)
- The Ramen Butcher (4.4 ⭐️)
- Maruhachi Ra-men Westend (4.6 ⭐️)
- Tonkotsu Ramen Tsukiya (4.8 ⭐️)
And 15 more ... As you can see, Text Search results are not based just on average rating or distance from the specified location. Instead, they are based on prominence, which is a measure of a business’s offline popularity. Google does not disclose the exact formula for calculating prominence, but the number and quality of reviews matters, as does the presence of relevant keywords in the listing description.
Google Places API Text Search pricing
Pricing for the Google Text Search API is straightforward. It costs $32 CPM (per thousand requests). This might seem high, but considering that you get valuable fields such as place_id
, rating
, user_rating_total
and photo_reference
included in the response without having to make a separate Place Details API call, it's actually quite reasonable. As always, you can contact a Google Maps Partner for bulk pricing and discounts.
Building a query engine powered by Google Text Search
In this last section, we'll use the Google Text Search API to build a Large Language Model (LLM) inspired query engine. Simply type what you're looking for into the search field (e.g., "Best ramen in Vancouver"), and the results will display on a map, complete with photos and user ratings (live demo: https://places-api-text-search.afi.dev/).
How our code is organized
The places_api_text_search app is a single-page React application. It features a /frontend
with a search box, map, and results listing, while the /backend
handles calls to the Google Text Search API. As with our other tutorials, we'll be using @vis.gl/react-google-maps to quickly scaffold our app. This library helps integrate Google Maps components into a React app by providing prebuilt components that make it easy to manage map interactions and UI in a way that is consistent with React. You can find full source code and installation instructions to run the app locally in our Github repository.
App.jsx
App.jsx
in the /frontend
folder serves as the main entry point for the application's frontend (above) and logic. It provides a clean, map-centered UI structure with a few key interactive components for our query engine.
/* frontend/App.jsx */
import React, { useState } from "react";
import { APIProvider } from "@vis.gl/react-google-maps";
import Map from "./components/Map";
import LocationSearchPanel from "./components/LocationSearchPanel";
import PlaceList from "./components/PlaceList";
import "./App.scss";
const App = () => {
const [places, setPlaces] = useState([]);
const [activePlace, setActivePlace] = useState({});
const [searchValue, setSearchValue] = useState();
const handlePlaceSelection = async (places) => {
setPlaces(places);
};
const handlePlaceClick = (place) => {
setActivePlace(place || {});
};
return (
<div className="app">
<APIProvider apiKey={process.env.REACT_APP_GOOGLE_API_KEY}>
<div className="location-panel">
<LocationSearchPanel
searchValue={searchValue}
onSearch={setSearchValue}
onPlaceSelect={handlePlaceSelection}
/>
<PlaceList
placeList={places}
activePlace={activePlace}
onPlaceClick={handlePlaceClick}
/>
</div>
<Map
places={places}
activePlace={activePlace}
onPlaceClick={handlePlaceClick}
/>
</APIProvider>
</div>
);
};
export default App;
At the top of App.jsx
, the <APIProvider/>
wrapper component provides Google Maps API context to its children, which enables components within it (such as <Map/>
) to access the Google Maps API using the REACT_APP_GOOGLE_API_KEY
.
<App/> (div.app)
|
|-- <APIProvider/> (Google Maps API context)
| |
| |-- Location Panel (div.location-panel)
| | |
| | |-- <LocationSearchPanel/>
| | | (TextSearch input field)
| |
| |-- <PlaceList/>
| | (Displays list of places)
|
|-- <Map/>
(Displays Google Map with markers for places)
LocationSearchPanel.jsx
LocationSearchPanel.jsx
is a container and presentational component that holds the <PlaceAutocompleteInput/>
Text Search input field and an LLM-inspired chat bubble.
When the user submits a query, it’s saved to the onSearch
prop, which bubbles up to App.jsx
and is assigned to searchValue
. This searchValue
is passed back to <LocationSearchPanel />
and then into useTypingEffect()
to simulate a ChatGPT-style typing effect.
/* frontend/components/LocationSearchPanel.jsx */
import React from "react";
import PlaceAutocompleteInput from "./PlaceAutocompleteInput";
import "./LocationSearchPanel.scss";
import { ReactComponent as TriangleBubble } from "../assets/images/triangle-bubble.svg";
import useTypingEffect from "../hooks/useTypingEffect";
const LocationSearchPanel = ({ onSearch, onPlaceSelect, searchValue }) => {
const typedText = useTypingEffect(searchValue, 10);
return (
<div className="location-search-panel">
<h3>Ask Google Maps Anything</h3>
<PlaceAutocompleteInput
onSearch={onSearch}
onPlaceSelect={onPlaceSelect}
/>
{!!typedText && (
<>
<div className="break-line"></div>
<div className="search-value">
<TriangleBubble />
{typedText}
</div>
</>
)}
</div>
);
};
export default LocationSearchPanel;
useTypingEffect.js
useTypingEffect.js
defines a custom React hook, useTypingEffect
, which creates a typewriter effect for a given text.
By convention, useTypingEffect
is treated as a custom hook simply because of the use
prefix. Because of this, whenever the value of the displayedText
state variable (the currently displayed portion of the full text string being typed out) is updated via setDisplayedText
, React re-renders the component using this hook, providing the latest displayedText
value to that component.
Here's how it works:
/* frontend/hooks/useTypingEffect.js */
import { useState, useEffect } from "react";
const useTypingEffect = (text, baseSpeed = 100) => {
const [displayedText, setDisplayedText] = useState("");
useEffect(() => {
if (!text) {
setDisplayedText("");
return;
}
let index = 0;
const typeCharacter = () => {
setDisplayedText(text.slice(0, index + 1));
index++;
if (index < text.length) {
const nextChar = text[index];
// Set a random speed, slower for spaces and punctuation
const randomSpeed =
baseSpeed +
(nextChar === " " || [".", ",", "!", "?"].includes(nextChar)
? Math.floor(Math.random() * 120) + 100 // Longer pause for spaces/punctuation
: Math.floor(Math.random() * 70)); // Smaller variation for other characters
setTimeout(typeCharacter, randomSpeed);
}
};
typeCharacter(); // Start typing effect
return () => setDisplayedText(""); // Clean up on text change or unmount
}, [text, baseSpeed]);
return displayedText;
};
export default useTypingEffect;
When useTypingEffect
is called, displayedText
is initialized as an empty string (""
) using useState
. Inside the typeCharacter()
function, displayedText
is updated by slicing the text
up to the current character index and setting this slice as the new value of displayedText
via setDisplayedText
. With each iteration, displayedText
grows one character at a time until it matches text
.
/* frontend/hooks/useTypingEffect.js */
const typeCharacter = () => {
setDisplayedText(text.slice(0, index + 1));
index++;
// ... rest of typeCharacter()
}
To make the typing effect more realistic, we introduce a random delay by recursively calling the setTimeout()
method with typeCharacter()
paired with a randomSpeed
value. Each time setTimeout()
is called, the value of index
is incremented by 1 until its value reaches the length of the text
string, signaling that the entire text has been displayed.
/* frontend/hooks/useTypingEffect.js */
const randomSpeed =
baseSpeed +
(nextChar === " " || [".", ",", "!", "?"].includes(nextChar) ?
Math.floor(Math.random() * 120) + 100 // Longer pause for spaces/punctuation
:
Math.floor(Math.random() * 70)); // Smaller variation for other characters
setTimeout(typeCharacter, randomSpeed);
PlaceAutocompleteInput.jsx
PlaceAutocompleteInput.jsx
contains the text input field and submit button that allows users to search Google Maps using free-form text. Instead of using Place Autocomplete, this component is configured to accept any input, which is then sent directly to the Google Maps Text Search API.
/* frontend/components/PlaceAutocompleteInput.jsx */
import React, { useState } from "react";
import { ReactComponent as SendIcon } from "../assets/images/send.svg";
import "./PlaceAutocompleteInput.scss";
const PlaceAutocompleteInput = ({ onSearch, onPlaceSelect }) => {
const [query, setQuery] = useState("");
const handleInputChange = (e) => {
setQuery(e.target.value);
};
const hasSufficientQueryLength = () => query.length > 2;
const handleSearch = async () => {
if (hasSufficientQueryLength()) {
onSearch(query);
setQuery("");
try {
const response = await fetch(
`${process.env.REACT_APP_BACKEND_URI}/places?query=${query}`
);
const suggestions = await response.json();
onPlaceSelect(suggestions);
} catch (error) {
console.error("Error fetching data:", error);
}
}
};
const handleKeyDown = async (e) => {
if (e.key === "Enter") {
e.preventDefault();
await handleSearch();
}
};
return (
<div className="autocomplete-container">
<input
type="text"
value={query}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="What are you looking for?"
/>
<SendIcon
onClick={handleSearch}
style={{ fill: hasSufficientQueryLength() ? "#2594ee" : "#888888" }}
/>
</div>
);
};
export default PlaceAutocompleteInput;
The handleSearch()
function is triggered when the user clicks the <SendIcon/>
or presses [Enter]. If the query
is more than 2 chars long, it's sent to the /places
endpoint in our backend for a pass-through call to the Google Text Search API.
placeController.js
The app's backend runs on an Express web server, listening on port 3001. Requests are routed to placeController.js
for handling.
/* backend/controllers/placeController.js */
const axios = require("axios");
exports.getPlacesWithDetails = async (req, res) => {
try {
const { query } = req.query;
if (!query) {
return res.status(400).json({ error: "Query parameter is required" });
}
const apiKey = process.env.GOOGLE_API_KEY;
// Step 1: Perform a text search to get a list of places
const textSearchResponse = await axios.get(
"https://maps.googleapis.com/maps/api/place/textsearch/json",
{
params: {
query,
key: apiKey,
},
}
);
const places = textSearchResponse.data.results;
// Step 2: Fetch additional details for each place
const detailedPlaces = await Promise.all(
places.map(async (place) => {
// Combine basic info with additional details
return {
id: place.reference,
name: place.name,
address: place.formatted_address,
latitude: place.geometry.location.lat,
longitude: place.geometry.location.lng,
rating: place.rating,
reviewsCount: place.user_ratings_total,
imageUrl: place.photos
? getPhotoUrl(place.photos[0].photo_reference, apiKey)
: "",
};
})
);
res.status(200).json(detailedPlaces);
} catch (error) {
console.error("Error fetching places with details:", error);
res.status(500).json({ error: "Internal server error" });
}
};
const getPhotoUrl = (photoReference, apiKey) => {
return `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${photoReference}&key=${apiKey}`;
};
When the query string hits placeController.js
, the first thing that happens is that we use axios.get()
to make an HTTP GET request to Google Text Search: https://maps.googleapis.com/maps/api/place/textsearch/json. Two parameters are supplied - query
, our search string e.g. "best ramen in Vancouver" and apiKey
, a working Google Maps API key.
/* backend/controllers/placeController.js */
const textSearchResponse = await axios.get(
"https://maps.googleapis.com/maps/api/place/textsearch/json", {
params: {
query,
key: apiKey,
},
}
);
Next, we save the initial text search response (an array of place objects) to the places
variable, and use it to create a new detailedPlaces
array that combines basic place information with additional details such at the place photo, retrieved from the Place Photos API.
/* backend/controllers/placeController.js */
const places = textSearchResponse.data.results;
const detailedPlaces = await Promise.all(
places.map(async (place) => {
return {
id: place.reference,
name: place.name,
address: place.formatted_address,
latitude: place.geometry.location.lat,
longitude: place.geometry.location.lng,
rating: place.rating,
reviewsCount: place.user_ratings_total,
imageUrl: place.photos ?
getPhotoUrl(place.photos[0].photo_reference, apiKey) :
"",
};
})
);
Finally, we return detailedPlaces
as a JSON object so that it can be used in the frontend to display place
listing on the map and left sidebar.
res.status(200).json(detailedPlaces);
PlaceList.jsx
Once the backend returns the list of places from the Text Search API, the result is passed to the onPlaceSelect
prop in LocationSearchPanel.jsx
. This then flows up to App.jsx
, where the handlePlaceSelection()
method updates the app state with the new places data.
/* frontend/App.jsx */
const handlePlaceSelection = async (places) => {
setPlaces(places);
};
It also passes the array, places
, to <PlaceList/>
via the prop placeList={places}
.
PlaceList.jsx
displays a list of places and handles user interactions, such as selecting or highlighting a place.
/* frontend/components/PlaceList.jsx */
return (
<div className="PlaceList">
{placeList.map((place) => (
<PlaceItem
key={place.id}
ref={(el) => (itemRefs.current[place.id] = el)}
{...place}
active={activePlace.id === place.id}
onClick={() => onPlaceClick(place)}
/>
))}
</div>
);
placeList.map()
iterates over each place
object in the placeList
array, rendering a <PlaceItem/>
component for each one. Each <PlaceItem/>
displays a small information panel that includes the place’s name, picture, average user rating, and the total number of reviews it has received on Google Maps.
This is done using the {...place}
spread operator which transfers all properties of place
as props on <PlaceItem/>
, giving <PlaceItem/>
direct access to the place
data. One of these props is rating
, the place's average user rating on Google Maps.
Rating.jsx
It's helpful to take some time to explain how star ratings are displayed in our app, as it uses the same approach as Google Maps for showing ratings.
/* frontend/components/Rating.jsx */
import React from "react";
import StarRateIcon from "../assets/images/star_rate.svg";
import HalfStarRateIcon from "../assets/images/star_half.svg";
const Rating = ({ rating = 0 }) => {
const ratingNum = +(rating || 0);
const fullStars = Math.floor(ratingNum);
const hasHalfStar = ratingNum % 1 > 0;
if (!ratingNum) return null;
return (
<>
{[...Array(fullStars)].map((_, index) => (
<img
key={`full-${index}`}
width={20}
alt="Star rate icon"
src={StarRateIcon}
/>
))}
{hasHalfStar && (
<img
key="half"
width={20}
alt="Half star rate icon"
src={HalfStarRateIcon}
/>
)}
</>
);
};
export default React.memo(Rating);
Star ratings ⭐️ on Google Maps and other review sites generally use a 5-point scale, where 5 is the highest rating and 1 is the lowest (there's no 0 rating, though sometimes it feels like there should be). For instance, a rating of 4.6 would display as 4 and a half stars, because a half star is shown whenever there’s a decimal component in the rating.
To do this, we first calculate how many full stars we should show using const fullStars = Math.floor(ratingNum);
. So Math.floor(4.6)
is 4.
Next, we check to see if there is a decimal component by using the modulo operator. const hasHalfStar = ratingNum % 1 > 0;
returns true
if there is any remainder modulo 1 (4.6 % 1 = 0.6, so yes).
Then, we create an iterable array of fullStars
elements (4 in our example) and render an <img/>
element each time. This results in a full star icon for each full star, showing a sequence of stars side-by-side.
/* frontend/components/Rating.jsx */
import StarRateIcon from "../assets/images/star_rate.svg";
{[...Array(fullStars)].map((_, index) => (
<img
key={`full-${index}`}
width={20}
alt="Star rate icon"
src={StarRateIcon}
/>
))}
This code is able to render the full star icon as an image by importing the SVG file as a module and then using it as the src
attribute in an <img>
tag. It works because bundlers like Webpack are configured to process static assets like images and SVGs, allowing you to import them directly into JavaScript files.
Finally we check to see if we should render a half star, and tag on a half star icon at the end.
/* frontend/components/Rating.jsx */
{hasHalfStar && (
<img
key="half"
width={20}
alt="Half star rate icon"
src={HalfStarRateIcon}
/>
)}
Map.jsx
Map.jsx
initializes a <Map/>
component centered on Vancouver, BC, displaying the location of each place on the map. To ensure that we are able to fit everything nicely when our backend returns a list of places
from the Google Text Search API, we do the following:
- Iterate over the
places
array to determine the "corner" southwest and northeast bounds, and save them to a Google MapsLatLngBounds
object.
/* frontend/components/Map.jsx */
const bounds = new window.google.maps.LatLngBounds();
places.forEach(({
latitude,
longitude
}) => {
bounds.extend({
lat: latitude,
lng: longitude
});
});
- Adjusts the viewport of the
<Map/>
component to fit within specific boundary constraints.
/* frontend/components/Map.jsx */
mapInstance.fitBounds(bounds, {
top: 20,
right: 20,
bottom: 20,
left: 400,
});
The final outcome is a map that displays all the <Marker/>
elements with a controlled amount of padding around them. This ensures that all the markers are visible on the map without being too crowded or too spread out.
The rest of <Map.jsx/>
contains code that iterates over the places
array and adds clickable <Marker/> components at each place.latitude
and place.longitude
. When clicked, each <Marker/>
opens up an <InfoWindow/> component with the name, address, picture and user rating of the selected place.
To test the app, simply visit https://places-api-text-search.afi.dev/ and enter any search term in the search box. If you'd like to run the app locally, fork the project on GitHub and run npm install
in both the /backend
and /frontend
folders to install dependencies. Start the app by running npm start
in each folder, then open http://localhost:3000 in your browser to view it. Remember to update the .env
files in both the backend and frontend with your Google Maps API key, ensuring that it has the Places API enabled.
Google Places API: Putting it all into place
This wraps up our tutorial series on the Google Maps Places API. We’ve covered not only how and when to use Place Autocomplete, Place Details, Place Photos, Nearby Search and Text Search APIs, but also demonstrated their practical application in building real-world, functional apps.
In today’s data-driven world, delivering accurate, real-time information is key to creating exceptional user experiences. Google Maps probably has more than a billion records in its vast and continuously updated database of businesses, landmarks, and other points of interest. These records are collected through a combination of user contributions, business owners managing their profiles and Google's own data collection efforts. The Google Maps Places API is the best way to use this data to add timely and reliable location-based information to your app.
Part 1: Finding the right place with the Google Places API
Part 2: Google address autocomplete with the Places API
Part 3: Google Place Details and Place Photos API
Part 4: Google Nearby Search API
Part 5: Google Places Text Search
👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.