Google Places Text Search

Using the Google Text Search API to help users quickly find places with simple, natural text queries.

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 - coming soon!) 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

  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 (coming soon!) 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 (coming soon!).

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

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.

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.

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.

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

  • PlaceAutoCompleteInput.jsx
  • backend/PlaceController.js
  • PlaceList.jsx
  • PlaceItem.jsx
  • Map.jsx

google text search

google places api text search example

google places api text search example

google places api text search

google maps text search api

google text search api

google places text search

text search google

text from google maps