> ## Content Index
> Fetch the complete content index at: https://blog.afi.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Google Places Text Search
- URL: https://blog.afi.io/blog/google-places-text-search/
- Published: 2024-05-31T15:09:00.000Z
- Updated: 2026-08-21T05:44:02.000Z
- Description: Using the Google Text Search API to help users quickly find places with simple, natural text queries.
- Author: Afian Anwar
- Tags: googlemaps

If you are building a mapping application, you need to know about the Google Maps [Text Search API](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io), 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](https://www.google.com/maps/place/555+W+Hastings+St,+Vancouver,+BC+V6B+4N4,+Canada/@49.2889938,-123.1152314,14.02z/data=!4m6!3m5!1s0x548671786785c251:0xa13de85753959110!8m2!3d49.2846966!4d-123.1119349!16s%2Fg%2F11b7q2hd1t?entry=ttu&g%5Fep=EgoyMDI0MTAyMy4wIKXMDSoASAFQAw%3D%3D&ref=blog.afi.io)” or even something more general like “[best dim sum in Richmond](https://www.google.com/maps/search/best+dimsum+in+richmond+bc/@49.1736486,-123.1627855,13.05z?entry=ttu&g%5Fep=EgoyMDI0MTAyMy4wIKXMDSoASAFQAw%3D%3D&ref=blog.afi.io)”. 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/10/Google-Map-AI--4-.png)

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](https://afi.io/blog/finding-the-right-place-with-the-google-places-api/?ref=blog.afi.io), 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](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io) ([demo](https://places-api-text-search.afi.dev/?ref=blog.afi.io) / [source code](https://github.com/afilabs/places%5Fapi%5Ftext%5Fsearch?ref=blog.afi.io)) 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](https://afi.io/blog/finding-the-right-place-with-the-google-places-api/?ref=blog.afi.io)  
Part 2: [Google address autocomplete with the Places API](https://www.afi.io/blog/google-address-autocomplete-with-the-places-api/?ref=blog.afi.io)  
Part 3: [Google Place Details and Place Photos API](https://www.afi.io/blog/google-place-details-and-place-photos-api/?ref=blog.afi.io)  
Part 4: [Google Nearby Search API](https://afi.io/blog/google-nearby-search-api/?ref=blog.afi.io)  
**Part 5: Google Places Text Search (this article)**

[Google Places API Text Search DemoDemo app for the Google Places Text Search API.![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/icon/logo192-26.png)![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/thumbnail/logo512-31.png)](https://places-api-text-search.afi.dev/?ref=blog.afi.io)

### What is the Google Places Text Search API?

The [Google Places Text Search API](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io), often called Google Text Search, allows you to find places based on keywords, rather than geographic coordinates or a `place_id`. This differs from the [Google Place Details API](https://afi.io/blog/google-place-details-and-place-photos-api/?ref=blog.afi.io), which specifically needs a `place_id` to work.

### Google Places API Text Search example

Similar to other [Google Maps Platform APIs](https://mapsplatform.google.com/?ref=blog.afi.io), Google Text Search works directly on [https://maps.google.com/](https://maps.google.com/?ref=blog.afi.io). To try it out, type in an address like "555 West Hastings Street, Vancouver" into the search bar at [https://maps.google.com/](https://maps.google.com/?ref=blog.afi.io) without selecting any of the Places Autocomplete suggestions, and hit the return key.

![Google Places API Text Search example](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/10/Screenshot-2024-10-29-at-10.06.44-AM.png)

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](https://developers.google.com/maps/documentation/places/web-service/supported%5Ftypes?ref=blog.afi.io) 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](https://afi.io/blog/finding-the-right-place-with-the-google-places-api/?ref=blog.afi.io).

The [Google Text Search API](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io) (see [documentation](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io)) 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](https://www.google.com/maps/place/555+W+Hastings+St,+Vancouver,+BC+V6B+4N4,+Canada/@49.2889938,-123.1152314,14.02z/data=!4m6!3m5!1s0x548671786785c251:0xa13de85753959110!8m2!3d49.2846966!4d-123.1119349!16s%2Fg%2F11b7q2hd1t?entry=ttu&g%5Fep=EgoyMDI0MTAyMy4wIKXMDSoASAFQAw%3D%3D&ref=blog.afi.io)” 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/10/image-6.png)

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](https://developers.google.com/maps/documentation/places/web-service/place-id?ref=blog.afi.io). It can be used to retrieve the place's name, address, photos, reviews etc using the [Place Details API](https://www.afi.io/blog/google-place-details-and-place-photos-api?ref=blog.afi.io).

### 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/10/Screenshot-2024-10-30-at-5.12.54-AM.png)

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](https://afi.io/blog/google-place-details-and-place-photos-api/?ref=blog.afi.io) 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/Screenshot-2024-11-01-at-11.16.00-PM.png)

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](https://maps.app.goo.gl/w8grWCYVyN4ijEo6A?ref=blog.afi.io) (4.4 ⭐️)
2. [Ramen Danbo Robson](https://maps.app.goo.gl/CsMdgcfsWfVEAQG69?ref=blog.afi.io) (4.6 ⭐️)
3. [The Ramen Butcher](https://maps.app.goo.gl/Xkt5zMpRTn8hL7o77?ref=blog.afi.io) (4.4 ⭐️)
4. [Maruhachi Ra-men Westend](https://maps.app.goo.gl/StBivRCxGbhQpQLr9?ref=blog.afi.io) (4.6 ⭐️)
5. [Tonkotsu Ramen Tsukiya](https://maps.app.goo.gl/frsiPfeENCUrhmRX8?ref=blog.afi.io) (4.8 ⭐️)

[google\_text\_search\_best\_ramen\_in\_vancouver\_responsegoogle\_text\_search\_best\_ramen\_in\_vancouver\_response.json42 KBdownload-circle](https://blog.afi.io/content/files/2024/11/google%5Ftext%5Fsearch%5Fbest%5Framen%5Fin%5Fvancouver%5Fresponse.json "Download")

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](https://afi.io/blog/google-place-details-and-place-photos-api/?ref=blog.afi.io) call, it's actually quite reasonable. As always, you can contact a [Google Maps Partner](https://cloud.google.com/find-a-partner/?products=Google+Maps+Platform&ref=blog.afi.io) 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](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io) 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/](https://places-api-text-search.afi.dev/?ref=blog.afi.io)).

0:00 

/0:13 

1× 

### How our code is organized

The [places\_api\_text\_search](https://github.com/afilabs/places%5Fapi%5Ftext%5Fsearch?ref=blog.afi.io) app is a single-page [React](https://react.dev/?ref=blog.afi.io) application. It features a `/frontend` with a search box, map, and results listing, while the `/backend` handles calls to the [Google Text Search API](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io). As with our other tutorials, we'll be using [@vis.gl/react-google-maps](https://visgl.github.io/react-google-maps/?ref=blog.afi.io) 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](https://react.dev/?ref=blog.afi.io). You can find full source code and installation instructions to run the app locally in our [Github repository](https://github.com/afilabs/places%5Fapi%5Ftext%5Fsearch?ref=blog.afi.io).

**App.jsx**

![Google Places API Text Search example app structure](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/image-1.png)

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.

```JSX
/* 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/Screenshot-2024-11-06-at-8.18.22-AM.png)

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.

```JSX
/* 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](https://legacy.reactjs.org/docs/hooks-intro.html?ref=blog.afi.io), `useTypingEffect`, which creates a typewriter effect for a given text. 

![A custom React hook to create a typewriter effect](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/google_text_search_blog_post-optimized-ezgif.com-cut.gif)

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

```javascript
/* 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.

```javascript
/* 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](https://developers.google.com/maps/documentation/places/web-service/autocomplete?ref=blog.afi.io), this component is configured to accept any input, which is then sent directly to the [Google Maps Text Search API](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io).

```
/* 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/Screenshot-2024-11-07-at-4.56.23-PM.png)

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

**placeController.js**

The app's backend runs on an [Express](https://expressjs.com/?ref=blog.afi.io) web server, listening on port 3001\. Requests are routed to `placeController.js` for handling.

```javascript
/* 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](https://maps.googleapis.com/maps/api/place/textsearch/json?ref=blog.afi.io). 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](https://developers.google.com/maps/documentation/places/web-service/photos?ref=blog.afi.io).

```javascript
/* 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.

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

```javascript
/* 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.

```JSX
/* 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/Screenshot-2024-11-09-at-12.12.45-AM.png)

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

This is done using the `{...place}` spread operatorwhich 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.

```JSX
/* 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.

```JSX
/* 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.

```JSX
/* 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](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io), 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](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2024/11/Screenshot-2024-11-09-at-7.43.11-PM.png)

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](https://visgl.github.io/react-google-maps/docs/api-reference/components/marker?ref=blog.afi.io) at each `place.latitude` and `place.longitude`. When clicked, each `<Marker/>` opens up an [<InfoWindow/> component](https://visgl.github.io/react-google-maps/docs/api-reference/components/info-window?ref=blog.afi.io) 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](https://afi.io/blog/google-address-autocomplete-with-the-places-api?ref=blog.afi.io) and [Nearby Search](https://afi.io/blog/google-nearby-search-api/?ref=blog.afi.io) blog posts.

To test the app, simply visit [https://places-api-text-search.afi.dev/](https://places-api-text-search.afi.dev/?ref=blog.afi.io) and enter any search term in the search box. If you'd like to run the app locally, [fork the project on GitHub](https://github.com/afilabs/places%5Fapi%5Ftext%5Fsearch?ref=blog.afi.io) 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](http://localhost:3000/?ref=blog.afi.io) 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](https://developers.google.com/maps/documentation/places/web-service/overview?ref=blog.afi.io). We’ve covered not only how and when to use [Place Autocomplete](https://developers.google.com/maps/documentation/places/web-service/autocomplete?ref=blog.afi.io), [Place Details](https://developers.google.com/maps/documentation/places/web-service/details?ref=blog.afi.io), [Place Photos](https://developers.google.com/maps/documentation/places/web-service/photos?ref=blog.afi.io), [Nearby Search](https://developers.google.com/maps/documentation/places/web-service/nearby-search?ref=blog.afi.io) and [Text Search](https://developers.google.com/maps/documentation/places/web-service/search-text?ref=blog.afi.io) APIs, but also demonstrated their practical application in building real-world, functional apps.

[Autocomplete Place DemoHow to use a Google Places autocomplete widget to validate addresses as a user types![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/icon/logo192-2.png)![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/thumbnail/logo512-1.png)](https://autocomplete-place.afi.dev/?ref=blog.afi.io)

[Places API DemoHow to use the Google Nearby Search API to find nearby places, businesses and points of interest.![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/icon/logo192-3.png)![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/thumbnail/logo512-2.png)](https://places-api-demo.afi.dev/?ref=blog.afi.io)

[Google Places API Text Search DemoDemo app for the Google Places Text Search API.![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/icon/logo192-1.png)![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/thumbnail/logo512.png)](https://places-api-text-search.afi.dev/?ref=blog.afi.io)

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](https://developers.google.com/maps/documentation/places/web-service/overview?ref=blog.afi.io) 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](https://afi.io/blog/finding-the-right-place-with-the-google-places-api/?ref=blog.afi.io)  
Part 2: [Google address autocomplete with the Places API](https://www.afi.io/blog/google-address-autocomplete-with-the-places-api/?ref=blog.afi.io)  
Part 3: [Google Place Details and Place Photos API](https://www.afi.io/blog/google-place-details-and-place-photos-api/?ref=blog.afi.io)  
Part 4: [Google Nearby Search API](https://afi.io/blog/google-nearby-search-api/?ref=blog.afi.io)  
Part 5: [Google Places Text Search](https://afi.io/blog/google-places-text-search/?ref=blog.afi.io)

**This article was written by** [**Afi Labs**](https://afi.io/?ref=blog.afi.io)**, a** [**Google Maps Premier Partner**](https://www.afi.io/?ref=blog.afi.io) **and reseller. We build route optimization, navigation, and fleet tracking software on Google Maps, and offer volume pricing on GMP licensing.** [**Talk to an engineer**](https://www.afi.io/contact%5Fus?ref=blog.afi.io) **or** [**follow Afian on LinkedIn**](https://www.linkedin.com/in/afian-anwar/?ref=blog.afi.io)**.**