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.

The query engine powered by the Google Text Search API that we will build

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.

Google Places API Text Search example

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"
}
The fields returned by the Google Places Text Search API

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".

Using the Google Places Text Search API to look up listings at a specific address

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.

Google Places API Text Search free-form text example

Method: GET

https://maps.googleapis.com/maps/api/place/textsearch/json?
query=best%20ramen%20in%20vancouver&
key=YOUR_API_KEY

Response

  1. Jinya Ramen Bar (4.4 ⭐️)
  2. Ramen Danbo Robson (4.6 ⭐️)
  3. The Ramen Butcher (4.4 ⭐️)
  4. Maruhachi Ra-men Westend (4.6 ⭐️)
  5. 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.

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/).

0:00
/0:13

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

Google Places API Text Search example app structure

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.

Google Places API Text Search input field and 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.

A custom React hook to create a typewriter effect

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.

How our Google Places API Text Search app works on the backend

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.

The Google Places Text Search API returns the name, picture, and rating of the place

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:

  1. Iterate over the places array to determine the "corner" southwest and northeast bounds, and save them to a Google Maps LatLngBounds object.
/* frontend/components/Map.jsx */
const bounds = new window.google.maps.LatLngBounds();
places.forEach(({
    latitude,
    longitude
}) => {
    bounds.extend({
        lat: latitude,
        lng: longitude
    });
});
  1. 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.

How our Google Places Text Search API query engine uses fitBounds to control the map view

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.

👨‍💻
If you want to learn more about <Marker/> and <InfoWindow/>, follow the worked examples in our Address Autocomplete and Nearby Search blog posts.

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.

Autocomplete Place Demo
How to use a Google Places autocomplete widget to validate addresses as a user types
Places API Demo
Create a Place finder application by Reactjs
Google Places API Text Search Demo
Demo app for the Google Places Text Search API.

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.