Google Nearby Search API

In previous blog posts, I showed how to retrieve information about a point of interest using the Google Maps Places Autocomplete, Place Details and Place Photos APIs. In this one, we will use the Google Nearby Search API (commonly known as the Nearby Places API) to build a real estate search app to help home buyers and renters get to know new neighborhoods. We'll go through a worked example (demo / source code) of how to use the Nearby Search API, discuss pricing and build the app!

What our real estate search app built on the Google Nearby Search API looks like

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 (this article)
Part 5: Google Places Text Search

The Nearby Search API lets you search for places within a specified area. You can refine your search by providing keywords or specifying the type of place you are looking for. There are two versions of the API:

  1. Older Version: Requires a GET request to the https://maps.googleapis.com/maps/api/place/nearbysearch endpoint. Read my blog post on using the Nearby Search API to build a restaurant search and discover app.
  2. Newer Version: Uses a POST request to https://places.googleapis.com/v1/places:searchNearby and is called "Nearby Search (New)." Very creative.

For this blog post, we'll be using the "Nearby Search (New)" API (docs).

💡
Have an existing application that uses the Places API and wondering if you should upgrade to Nearby Search (New)? Probably not. However, if you are building a new app and want to take advantage of a vastly expanded list of place types e.g. sushi_restaurant, as well as advanced features such as the ability include or exclude specific types, go ahead!

Google Nearby Search API example

I'll now go through a worked example of the Google Nearby Search API using Pinnacle@Duxton as our search location. The Pinnacle@Duxton is a public housing project in Singapore where apartments can easily sell for around $1.5 mil SGD ($1.15 mil USD). It's in very high demand due to its central location near public transport and close proximity to the many restaurants, bars and cafes in the Chinatown and posh Duxton Hill neighborhoods.

A Google Maps entry for Pinnacle@Duxton, an expensive public housing project in Singapore

Let's say you are a prospective home buyer and want to check out what food options are nearby. The first step is to get the latitude and longitude of The Pinnacle. This is pretty easy . Go to Google Maps, enter Pinnacle@Duxton into the search bar and copy the GPS coordinates from the URL.

Retrieving the latitude and longitude of a Google Maps entry

Next, use the measure distance tool to visually approximate how large you want your search radius to be. It looks like 500 meters is sufficient to include most of the nearby restaurants and cafes.

500 meter search radius measured using the Google Maps ruler tool

Lastly, we need to check the Places API documentation to see what types of places we can search for. Since we are only interested in food, we'll restrict our search to "bar", "cafe" and "restaurant".

List of available types in the Google Places API

Now we are ready to make our first call to the Google Places Nearby Search API.

Endpoint POST https://places.googleapis.com/v1/places:searchNearby 

Headers
Content-Type: application/json; charset=utf-8
X-Goog-Api-Key: YOUR_API_KEY
X-Goog-FieldMask: places.displayName,places.id,places.types,places.formattedAddress,places.photos,places.priceLevel,places.userRatingCount,places.rating

Body

{
    "includedTypes": [
        "restaurant", "cafe", "bar"
    ],
    "maxResultCount": 1,
    "locationRestriction": {
        "circle": {
            "center": {
                "latitude": 1.2769282,
                "longitude": 103.8363251
            },
            "radius": 500
        }
    }
}

Here's the response from the Nearby Search API. For brevity, I've limited the maxResultCount to 1. In a real production application, this value would likely be set to at least 10 (the default and maximum value of maxResultCount is 20). There's no additional cost for returning more places.

Output

{
    "places": [
        {
            "id": "ChIJE7ZtOWwZ2jERFcasWYmljJQ",
            "types": [
                "coffee_shop",
                "cafe",
                "store",
                "food",
                "point_of_interest",
                "establishment"
            ],
            "formattedAddress": "4 Everton Park, #01-40, Singapore 080004",
            "rating": 4.7,
            "priceLevel": "PRICE_LEVEL_INEXPENSIVE",
            "userRatingCount": 1827,
            "displayName": {
                "text": "Nylon Coffee",
                "languageCode": "en"
            },
            "photos": [
                {
                    "name": "places/ChIJE7ZtOWwZ2jERFcasWYmljJQ/photos/AelY_CtKgWTv_oiurjqVdgdjqOZ5wOlZ44iaf0xxAyH5vjmRg5_wlACSnP9ha3tS57sPdA5mXb6TPMq6tIhyY4vKqEMva75e48TSDxRWOkwMX5PW2Y4v7riILfoL3uDzMmxyqT8-WTfnv3O7ddYd0-Em4nwzBah9rFzvlboS",
                    "widthPx": 3996,
                    "heightPx": 2249,
                    "authorAttributions": [
                        {
                            "displayName": "Nylon Coffee",
                            "uri": "//maps.google.com/maps/contrib/104310332821424592273",
                            "photoUri": "//lh3.googleusercontent.com/a-/ALV-UjW-vt45Gb_A1cebaEMsNCrdW5eu9c5meEpPaJzbYfOSu_C5C_T2=s100-p-k-no-mo"
                        }
                    ]
                },
                // ... 9 more similar entries
            ]
        }
    ]
}

The key element in the returned JSON is places, an array of place objects that contains detailed information about a single place. Results are ranked based on three criteria:

  • Prominence: This is a measure of how well-known or significant a place is. It considers the popularity and importance of a place. More popular and significant places are ranked higher. Note: Google does not make this prominence metric publicly available.
  • Proximity: The distance of the place from the specified location in the request. Closer places are generally ranked higher.
  • User Ratings and Reviews: Places with higher ratings and more reviews are likely to be ranked higher. The quality and recency of the reviews also play a role.
Mapping of fields in the results returned by the Google Nearby Search API

In the example above, the Places API returned Nylon Coffee, one of my all time favorite cafes, as its top result.

Within each place object,

id is the place ID of the place. The place ID uniquely identifies a place in the Google Map's database, and can be used to retrieve the place's name, address, photos, reviews etc.

types is an array of place types that can be used to label the place e.g. ["cafe", "bar"] might be used to describe a hip cafe that also serves beer. Types are inferred from the Google My Business categories that businesses select when creating a listing on Google Maps. Because submitting an accurate category helps potential customers find their business, listing owners are generally honest about the categories that best describe their business.

💡
The new Places API supports include/exclude filters that makes it easy to be specific about the kind of place you are looking for. Hungry but hate sushi? Try adding: "includedTypes": ["restaurant"], "excludedTypes": ["sushi_restaurant"] to your request.

formattedAddress is the nicely formatted address used in the place's Google Maps listing.

rating is the average rating provided by Google Maps users.

priceLevel is how expensive the restaurant is relative to others in the same general location. It ranges from "PRICE_LEVEL_INEXPENSIVE" ($) to "PRICE_LEVEL_VERY_EXPENSIVE" ($$$$ - docs). I've never eaten at a "PRICE_LEVEL_VERY_EXPENSIVE" restaurant before, but Elon Musk probably has.

userRatingCount is the number of Google Maps users who have left a rating for this business.

displayName is the name of the place shown on Google Maps (usually this is submitted by the listing owner, but sometimes a Google Maps employee will improve it for clarity).

photos is an array of listing owner and publicly submitted photos of the place. Each photo object contains a photoURI that can be used to retrieve a thumbnail of the photo using the Place Photos API.

💡
Each of the fields listed above were returned in the response because we asked them to be included using the HTTP header X-Goog-FieldMask.

Google Nearby Search API Pricing

There are three pricing tiers for the new Places API - Basic ($32 CPM), Advanced ($35 CPM) and Preferred ($40 CPM). Which pricing tier your API call falls under depends on which fields you included in the X-Goog-FieldMask header.

Basic: This version of the Places API returns you a basic set of information about the area of interest which includes places.name, places.photos, and places.id.

Advanced: The advanced tier allows users to display additional information about a place including places.priceLevel, places.rating, places.regularOpeningHours.

Preferred: This highest tier includes extra, value added information that your users might be willing to pay extra for. These include places.allowsDogs, places.curbsidePickup, and places.delivery.

🌎
You can find an exhaustive list of place fields and their corresponding tiers at the SKU: Nearby Search usage and billing guide.

You are only billed for the Places API SKU you consume e.g. if you select any fields from the Place Details (Preferred) SKU, then you are billed at $40 CPM, or $0.04 per request. In our example above, the response returned included priceLevel and rating , so we will be billed at $35 CPM for the advanced tier.

Building a real estate search app using the Google Nearby Search API

We've seen how the Nearby Search API allows us to search for places within a specified area around a given location. This is a perfect fit for a real estate search app that lets potential home buyers and renters find points of interest, businesses, or services in neighborhoods they are interested in. Here's how our app (live demo: https://places-api-demo.afi.dev/) works:

0:00
/0:09

The user enters his desired address into a Google Maps Place Autocomplete search bar, which then brings up the location and highlights amenities such as bars, restaurants and supermarkets within a 500 meter radius.

A search radius of 500 meters applied to our Google Nearby Search API call

Photos, ratings, and user reviews for each amenity are available through the Google Maps Place Details API. Users can click on any listing to view more information and open the listing's page directly on Google Maps.

How our code is organized

We'll be using the new Google Maps React components from @vis.gl/react-google-maps to quickly set up our app. This library simplifies the integration of Google Maps components into a React web app. While it's not the first React component library for Google Maps, it is the only one that's officially supported.

The primary advantage of using a component library is speed. It's easy to find examples and code snippets online that you can quickly incorporate into your app. However, the downside is that the library abstracts much of the work involved in authenticating and calling the Google Maps API, which can result in a loss of control and flexibility in your app's functionality. Additionally, since react-google-maps is just a wrapper around the older Maps JavaScript API, it doesn't support features available in newer versions of the Places API, like specifying multiple types in a Nearby Search.

Afi Labs can help you build apps that use the Google Places API or other Google Maps APIs. 👋 Say Hello! to start working together.

Like all our tutorials, source code for this project is freely available on Github.

Getting started with Google Nearby Search in production

places-api-demo is a single page React app built on top of Autocomplete example code from the react-google-maps library. Here's how our app is organized:

src
 ├── App.jsx
 ├── /Components
 │   ├── /Map
 │   │   ├── index.jsx
 │   │   ├── MapHandler.jsx
 │   │   ├── Marker.jsx
 │   ├── /Constants
 │   │   ├── index.jsx
 │   ├── PlaceAutocompleteInput.jsx
 │   ├── PlaceItem.jsx
 │   ├── PlaceList.jsx
 │   ├── PlaceSearch.jsx
 │   ├── PlaceTypeSelector.jsx
 │   ├── Rating.jsx.jsx

App.jsx

Overview of how our React components are organized in our real estate app

App.jsx is the main entry point into the app. It handles the overall structure, routing, and layout (above).

/* App.jsx */
return (
  <APIProvider apiKey={process.env.REACT_APP_GOOGLE_API_KEY}>
    <div className="control-panel">
      <label>Your location</label>
      <PlaceSearch
        onSelectPlace={setMyLocation}
        onNearbyResultsReceived={setAllResults}
      />
      <PlaceTypeSelector
        options={placeTypeOptions}
        onSelectionChange={setSelectedPlaceTypes}
        selectedOptions={selectedPlaceTypes}
      />
      <PlaceList
        placeList={filteredPlaces}
        activePlace={activePlace}
        onPlaceClick={handlePlaceClick}
      />
    </div>
    <Map
      myLocation={myLocation}
      places={filteredPlaces}
      activePlace={activePlace}
      onMarkerClick={handlePlaceClick}
    />
  </APIProvider>
);

<APIProvider/> is the component that loads the Google Maps JavaScript API and provides context and functions to other components and hooks in the library. To work, <APIProvider/> only requires the Google Maps Platform API Key, which must be supplied via the apiKey prop. In our code, the API key is stored in the .env file located in the project's root directory.

PlaceSearch.jsx

PlaceSearch.jsx does several things. First, it is a container component that passes location and place information from the autocomplete widget in <PlaceSearch/> to the parent <App/>. The <Map/> component listens for changes to the value of myLocation and allResults, and updates itself accordingly.

/* PlaceSearch.jsx */
import { useMap, useMapsLibrary } from '@vis.gl/react-google-maps';

import PlaceAutocompleteInput from './PlaceAutocompleteInput';
import { PLACE_TYPES } from '../Constants';
import './PlaceSearch.scss';

const PlaceSearch = ({ onSelectPlace, onNearbyResultsReceived }) => {
  const map = useMap();
  const googleMaps = useMapsLibrary('places');

  const [placeService, setPlaceService] = useState(null);
  const [autocompleteToken, setAutocompleteToken] = useState(null);

  useEffect(() => {
    if (googleMaps && map) {
      setPlaceService(new googleMaps.PlacesService(map));
      setAutocompleteToken(new googleMaps.AutocompleteSessionToken());
    }
  }, [map, googleMaps]);
}

Second, it calls the useMap() and useMapLibrary() hooks to directly access the google.map.Map instance in <APIProvider/> as well as various Maps JavaScript API libraries. Once this is done, you can directly access the PlacesLibrary API and all the methods it contains with a two lines of code: const googleMaps = useMapsLibrary('places'); and setPlaceService(new googleMaps.PlacesService(map));.

Third, it generates a new Place Autocomplete session token to group the query and selection phases of the autocomplete search into a single session for accurate billing: setAutocompleteToken(new googleMaps.AutocompleteSessionToken());

Place Autocomplete widget used to retrieve address suggestions

When the user selects an address from the autocomplete widget in <PlaceSearch/>, the handlePlaceSelect() function gets called:

/* PlaceSearch.jsx */
const handlePlaceSelect = useCallback(
  (place) => {
    if (!place || !placeService) return;

    placeService.getDetails(
      {
        placeId: place.place_id,
        fields: ["geometry", "name", "formatted_address", "type"],
        sessionToken: autocompleteToken,
      },
      (result) => {
        onSelectPlace(result);
        searchNearbyPlaces(result);
        setAutocompleteToken(new googleMaps.AutocompleteSessionToken());
      },
    );
  },
  [placeService, onSelectPlace, searchNearbyPlaces, autocompleteToken],
);

handlePlaceSelect() uses the useCallback hook to memoize the function so that it is only re-created when one of its dependencies changes.

The place object returned by the getDetails() method of the Place Autocomplete API looks like this:

{
    "formatted_address": "685 Pacific Blvd, Vancouver, BC V6B 0R4, Canada",
    "geometry": {
        "location": {
            "lat": 49.2771031,
            "lng": -123.1088116
        },
        "viewport": {
            "south": 49.2757106197085,
            "west": -123.1099146302915,
            "north": 49.2784085802915,
            "east": -123.1072166697085
        }
    },
    "name": "685 Pacific Blvd",
    "place_id": "ChIJU5dionxxhlQROLRdJPfVT60",
    "types": [
        "premise"
    ],
    "html_attributions": []
}

This information is used in two ways. First, the onSelectPlace() method sends the place object to the parent component's setMyLocation state handler in App.jsx. Then, the Map.jsx component detects changes to the myLocation state and automatically centers the Google Map on the new location coordinates.

👨‍💻
For an in depth look at Google Places Autocomplete, see our tutorial on the Google address autocomplete with the Places API.

Second, the place object is used to call the searchNearbyPlaces() method:

/* PlaceSearch.jsx */
const searchNearbyPlaces = useCallback(
  (placeDetails) => {
    if (!placeService || !placeDetails.geometry) return;

    const allTypes = new Set(Object.keys(PLACE_TYPES));

    placeService.nearbySearch(
      {
        location: placeDetails.geometry.location,
        radius: 500,
        includedTypes: [...allTypes],
      },
      (results) => {
        console.log(
          results.map((p) => ({
            types: p.types.join(", "),
            vicinity: p.vicinity,
            matchedType: p.types.find((t) => allTypes.has(t)),
          })),
        );
        const filteredPlaces = results
          .map((place) => ({
            ...place,
            type: place.types.find((t) => allTypes.has(t)),
          }))
          .filter((place) => place.type);

        onNearbyResultsReceived(filteredPlaces);
      },
    );
  },
  [placeService, onNearbyResultsReceived],
);

Here, we call the Nearby Search API by passing in the location, radius and includedTypes parameters. includedTypes is an array of location types retrieved using import { PLACE_TYPES } from '../Constants';. It contains an array of strings such as "atm", "bank", "bar" etc that match place types we'd like to have near our location. Here's what a single result returned from the Nearby Search API looks like:

{
    "business_status": "OPERATIONAL",
    "geometry": {
        "location": {
            "lat": 49.2801271,
            "lng": -123.109935
        },
        "viewport": {
            "south": 49.2788225697085,
            "west": -123.1113308802915,
            "north": 49.28152053029149,
            "east": -123.1086329197085
        }
    },
    "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": "Chambar Restaurant",
    "opening_hours": {
        "open_now": true
    },
    "photos": [
        {
            "height": 1365,
            "html_attributions": [
                "<a href=\"https://maps.google.com/maps/contrib/107770196061871220304\">Chambar Restaurant</a>"
            ],
            "width": 2048
        }
    ],
    "place_id": "ChIJq2tI6HtxhlQRBaKUv59cS9E",
    "plus_code": {
        "compound_code": "7VJR+32 Vancouver, BC, Canada",
        "global_code": "84XR7VJR+32"
    },
    "price_level": 3,
    "rating": 4.5,
    "reference": "ChIJq2tI6HtxhlQRBaKUv59cS9E",
    "scope": "GOOGLE",
    "types": [
        "restaurant",
        "bar",
        "cafe",
        "food",
        "point_of_interest",
        "establishment"
    ],
    "user_ratings_total": 3931,
    "vicinity": "568 Beatty Street, Vancouver",
    "html_attributions": [],
    "type": "restaurant"
}

If the place types match i.e. type: place.types.find((t) => allTypes.has(t)), we pass them to the onNearbyResultsReceived prop for App.jsx to display on the map and place listing.

Components/Map/index.jsx

Components/Map/index.jsx (I'll refer to this as Map.jsx from now on) is responsible for rendering and managing a Google Map instance within our React application. It provides a React-friendly way to embed and interact with Google Maps. It handles the initialization and configuration of the map, such as setting the initial center, zoom level, and other map options as well as allowing us to add various map features, such as markers, info windows, polygons, and other overlays by adding these features as children of the GMap component.

/* Components/Map/index.jsx */
import React, { useEffect } from "react";
import { useMap, Map as Gmap } from "@vis.gl/react-google-maps";

import MapHandler from "./MapHandler";
import Marker from "./Marker";

const Map = ({ myLocation, places, activePlace, onMarkerClick }) => {
  const map = useMap();

  const handleMarkerClick = (place) => {
    onMarkerClick(place);
  };

  useEffect(() => {
    if (activePlace?.place_id) {
      map.panTo(activePlace.geometry?.location);
    }
  }, [activePlace, map]);

  return (
    <>
      <Gmap
        mapId={process.env.REACT_APP_GOOGLE_MAP_ID}
        defaultZoom={13}
        defaultCenter={{ lat: 22.27959367341436, lng: 114.17736930896572 }}
        gestureHandling={"greedy"}
        disableDefaultUI={true}
      >
        <MapHandler map={map} place={myLocation} />
        {myLocation && (
          <Marker
            style={{ width: 30 }}
            key="home"
            type="Home"
            position={myLocation.geometry?.location}
          />
        )}
        {places?.map((place) => (
          <Marker
            active={activePlace.place_id === place.place_id}
            style={{ width: 30 }}
            key={place.place_id}
            type={place.type}
            position={place.geometry?.location}
            onToggle={() => handleMarkerClick(place)}
          />
        ))}
      </Gmap>
    </>
  );
};

export default Map;

When the user selects a new location from the autocomplete widget, the location's coordinates are sent back to the parent App.jsx, and passed to Map.jsx via the activePlace prop:

/* App.jsx */
<Map myLocation={myLocation} places={filteredPlaces} activePlace={activePlace} onMarkerClick={handlePlaceClick} />

Whenever the user selects a new location and activePlace is updated, the useEffect() hooks in Map.jsx and MapHandler.jsx automatically pan to and centers the map on that location.

/* Components/Map/index.jsx */
useEffect(() => {
  if (activePlace?.place_id) {
    map.panTo(activePlace.geometry?.location);
  }
}, [activePlace, map]);
/* Components/Map/MapHandler.jsx */
useEffect(() => {
  if (!map || !place) return;

  if (place.geometry?.viewport) {
    map.setCenter(place.geometry?.location);
    map.setZoom(17);
  }
});

MapHandler.jsx

/Components/Map/MapHandler.jsx is a child of component of Map.jsx. It's only job is to wait for updates to activePlace, pan to the new activePlace.geometry.location, and draw a circle around it to represent the search radius of our call to the Nearby Search API.

/* Components/Map/MapHandler.jsx */
const updateCircle = () => {
  if (circleRef.current) {
    circleRef.current.setCenter(place.geometry?.location);
  } else {
    circleRef.current = new window.google.maps.Circle({
      strokeColor: "rgba(0, 148, 255, 1)",
      strokeOpacity: 0.8,
      strokeWeight: 2,
      fillColor: "rgba(0, 148, 255, 0.35)",
      fillOpacity: 0.35,
      map,
      center: place.geometry?.location,
      radius: 500,
    });
  }
};

The circle is given a blue outline and semi transparent fill rgba(0, 148, 255, 0.35) like so:

A circle drawn on Google Maps using new window.google.maps.Circle() function

Lastly, we add it to the <GMap/> component by passing map into the constructor of window.google.maps.Circle().

Marker.jsx

The <Marker/> component, located in /Components/Map/marker.jsx, is nested within the <GMap/> component. It allows us to add an AdvancedMarkerElement to the map which by default, appears as the red, balloon-shaped pin we all know and love. But it also, allows for customization, which I'll show you how to do below.

<Marker/> accepts the following props:

id: a unique identifier - for convenience we use place_id.
style: additional styling, if needed e.g. { width: 30 }.
position: the geographical coordinates (latitude and longitude) of the marker.
active: boolean indicating whether this marker is currently active or selected.
onToggle: function to be called when the marker is clicked.
type: string representing the type of place or marker, which determines the icon to be used.

/* Components/Map/MapHandler.jsx */
import React from "react";
import { AdvancedMarker } from "@vis.gl/react-google-maps";
import { ReactComponent as HomeMarker } from "../../assets/images/home_marker.svg";
import { PLACE_TYPES } from "../../Constants";
import "./Marker.scss";

const Marker = ({ id, style, position, active, onToggle, type }) => {
  const Icon = type === "Home" ? HomeMarker : PLACE_TYPES[type].markerIcon;

  return (
    <AdvancedMarker
      key={id}
      position={position}
      className={`marker ${active ? "active" : ""}`}
      zIndex={active ? 2 : 1}
      onClick={onToggle}
    >
      <Icon style={style} />
    </AdvancedMarker>
  );
};

export default Marker;

The <Icon/> component in <Marker/> determines which SVG to use. Here's how it works:

How icons are stored in the assets/images folder of our real estate search app

const Icon = type === 'Home' ? HomeMarker : PLACE_TYPES[type].markerIcon; checks to see if type is "Home". If it is, it assigns HomeMarker to the Icon variable. This is the equivalent of:

let Icon;

if (type === 'Home') {
  Icon = HomeMarker;
} else {
  Icon = PLACE_TYPES[type].markerIcon;
}

And in the return statement, <Icon style={style} /> now becomes <HomeMarker style={style} />.

But how exactly did we draw the custom home icon on the map? For this, we use inline SVG directly in React components by importing the SVG directly with: import { ReactComponent as HomeMarker } from '../../assets/images/home_marker.svg';. The ReactComponent import syntax is specific to Create React App (the build tool we used to scaffold our app) and other React setups that support this feature. It allows the SVG to be imported as a React component, not just as a static image.

If the type isn't "Home", then it looks up the corresponding marker icon in the PLACE_TYPES object using type as the key and assigns it to the Icon variable. PLACE_TYPES is an object retrieved from /Constants/index.js, made available via the import { PLACE_TYPES } from '../../Constants'; statement.

/* Constants/index.js */
import { ReactComponent as BankIcon } from '../assets/images/bank_icon.svg';

export const PLACE_TYPES = {
  atm: { label: 'ATM', dropdownIcon: BankIcon, markerIcon: BankMarker },
  bank: { label: 'Bank', dropdownIcon: BankIcon, markerIcon: BankMarker },
  bar: { label: 'Bar', dropdownIcon: BarIcon, markerIcon: BarMarker },
  // ... 14 more entries
};

So PLACE_TYPES['atm'].markerIcon; resolves to BankMarker, which is embedded as an SVG via import { ReactComponent as BankIcon } from '../assets/images/bank_icon.svg';.

To check that this works, you can add the line const IconTest = PLACE_TYPES['atm'].markerIcon; to Marker.jsx and replace the <Icon/> component with <IconTest/> i.e. <IconTest style={style} />. Rerun the app and you should now see each point of interest marker use the 🏦 icon.

Customizing the <AdvancedMarker/> component on Google Maps

PlaceTypeSelector.jsx

To make it easy to see at a glance what amenities are nearby, we group the results returned by the Nearby Search API and display them in a multi select box from react-select. The first step is to pick out the unique place types from the list of places returned by Nearby Search and match them with the types we are interested in from PLACE_TYPE_OPTIONS as defined in /Constants/index.js.

/* Constants/index.js */
export const PLACE_TYPE_OPTIONS = Object.keys(PLACE_TYPES).map((key) => ({
  value: key,
  label: PLACE_TYPES[key].label,
  icon: PLACE_TYPES[key].dropdownIcon,
}));

The next bit of code maps over allResults (the results from Nearby Search) to create a new array that consists only of the type properties of each result. Next, [...new Set(...)] creates a new set with these types, which automatically removes any duplicates because a Set only allows unique values. The spread operator (...) is then used to convert this Set back into an array, which contains only the unique types from the original allResults.

/* Utils/index.js */
export const getAvailablePlaceTypeOptions = (allResults, placeTypeOptions) => {
  const uniqueTypes = [...new Set(allResults.map((result) => result.type))];
  return placeTypeOptions.filter((option) =>
    uniqueTypes.includes(option.value),
  );
};

Once this is done, the function returns an array of filtered placeTypeOptions that match the types found in allResults. So if you are only looking for entries that match type "bar", "cafe" and "restaurant" and are allResults is made up of 10 "hotel", 2 "bar" and 1 "restaurant" entries, the output of our function would be an array: ["bar", "restaurant"].

Let's call the output placeTypeOptions. This is passed into <PlaceTypeSelector/> via the options prop:

/* App.jsx */
<PlaceTypeSelector
  options={placeTypeOptions}
  onSelectionChange={setSelectedPlaceTypes}
  selectedOptions={selectedPlaceTypes}
/>;

And displayed in the multi select field of the <Select/> component.

Using the react-select library to display place types

PlaceList.jsx

PlaceList.jsx is simpler compared to other React components in our app. It takes an array of place objects, placeList, as a prop. Each place object already conforms to the types defined in /Constants/index.js."

Place objects returned by the Nearby Search API

In the return statement of PlaceList.jsx, we iterate over placeList and use the spread operator, {...place}) to inject all properties of the place object as props to <PlaceItem/>, which means every key-value pair in place becomes a prop of <PlaceItem/>.

<PlaceItem/> component props

PlaceItem.jsx

PlaceItem.jsx is a child component of PlaceList.jsx, and it is tasked with displaying the data for each place as an individual entry. The name, rating, type, user_ratings_total and vicinity props are each assigned to their respective HTML elements for display.

Mapping of fields returned by the Nearby Search API to displayed attributes in search results

To test the app, visit https://places-api-demo.afi.dev/ and enter any address to see what amenities are available nearby. To run this app locally, all you need to do is fork it from Github and run npm install followed by npm start. You will also need to update the .env file with your Google Maps API key.

What's next

In this blog post, we utilized multiple Google Places APIs to create a real estate search app that shows nearby amenities. We specifically used the Google Nearby Search API to limit the search radius, filter results by type, and retrieve photos, ratings, and other valuable information about amenities in the chosen area."

In the next post, we'll look at the last API in this tutorial series, Text Search.

👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.

Part 5: Google Text Search API