Creating a Smart Address Search with Google Maps API and React

domanskyi

Valentyn Domanskyi

Posted on October 7, 2024

Creating a Smart Address Search with Google Maps API and React

Google provides robust access to its Maps API, which can be leveraged for various location-based functionalities and map-related tasks. In this article, I will explain how we utilized the Google AutocompleteService API to build a Smart Address Search field that replaces a full address form.

By using this approach, we reduced user input errors and improved the user experience by simplifying the address entry process, making it quicker and more accurate (1 smart input vs 3, huh).

AutocompleteService working demo

Step 1: Google Maps API Setup in React

First of all, to work with the Maps API you will need to enable it in Google Maps Platform and get the API key.

In our project, we use @react-google-maps/api package (npm i @react-google-maps/api).

Let's initialize the SDK with the custom useGoogleMapsApi hook, to make it reusable within the application:

const useGoogleMapsApi = () => {
  const api = useJsApiLoader({
    id: "app",
    googleMapsApiKey: "", // set your Public key
  });

  return {
    ...api,
  };
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Place Search with Google Autocomplete

Places Autocomplete Service provides an API to search within the places. It provides 2 methods:

  • getQueryPredictions() returns results, that do not necessary have the "place_id". It might also include search terms or groups of places like restaurants.
  • getPlacePredictions() returns precise places and administrative entities.

To implement our search, we used the getPlacePredictions() method. Let's add this method to our code and return it from the hook.

// Function to search for places using Google Autocomplete Service
const searchPlaces = async (
  query: string,
  options?: Omit<google.maps.places.AutocompletionRequest, "input">
): Promise<Array<google.maps.places.AutocompletePrediction>> => {
  // Load AutocompleteService from Google Maps API
  const { AutocompleteService } = (await google.maps.importLibrary(
    "places"
  )) as google.maps.PlacesLibrary;
  const autocomplete = new AutocompleteService();

  try {
    // Fetch place predictions based on user query
    const { predictions } = await autocomplete.getPlacePredictions({
      ...options, // Optional additional parameters for more precise searches
      input: query, // User's search query (e.g., "Baker Street")
    });

    // Return the list of predictions to display to the user
    return predictions;
  } catch {
    // If there's an error, return an empty array
    return [];
  }
};
Enter fullscreen mode Exit fullscreen mode

AutocompletionRequest interface uses the input field as a search query and other options, which allows to make a search more accurate. We will talk about them later.
We will use a simple input and a list of results for our demo.

import useGoogleMapsApi from "./useGoogleMapsApi";
import { useEffect, useState } from "react";

export default function App() {
  const { searchPlaces } = useGoogleMapsApi();
  // State to store input value from the user
  const [inputValue, setInputValue] = useState<string>("");

  // State to store the list of place predictions from Google Autocomplete
  const [places, setPlaces] = useState<
    Array<google.maps.places.AutocompletePrediction>
  >([]);

  // Function to handle the search process when the user types a query
  const handleSearch = async (searchQuery: string) => {
    const result = await searchPlaces(searchQuery);
    setPlaces(result);
  };

  // Trigger the search whenever the input value changes
  useEffect(() => {
    handleSearch(inputValue);
  }, [inputValue]);

  return (
    <div style={{ maxWidth: "80%", width: "100%",fontFamily: "sans-serif",}}>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Find an address"
      />
      {places.map((place) => (
        <div style={{ marginBottom: "0.5rem",}}>
          <span style={{ color: "blue", cursor: "pointer",}}>
            {place.description}
          </span>
          <span style={{ color: "#333", fontSize: "0.75rem",}}>{`(${place.place_id})`}</span>
          <span> - {place.types.join(", ")}</span>
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

AutocompleteService prediction blocks

From the predictions, we are interested in the 3 fields:

  1. Place full name.
  2. Place id.
  3. Place types - the array of types the entity belongs to. You can find a full list of types here. We use them for the additional results filtering.

Step 3: Refining Autocomplete Results

AutocompleteService interface has a bunch of inputs, that allow make a search more accurate.

Field componentRestrictions allows us to narrow the search results to one specific country. It supports up to 5 countries and requires country codes to be in ISO 3166-1 Alpha-2 standard. Here you can find a list of countries' codes.

    const { predictions } = await autocomplete.getPlacePredictions({
      ...options,
      input: query,
      componentRestrictions: {
        country: ["gb"],
      },
    });
Enter fullscreen mode Exit fullscreen mode

To make our field not confusing for the users, we need to exclude places like parks, airports, etc from the search results. The Autocomplete service has a field types to specify the types of predictions to be returned. But only one type is allowed for the AutocompleteService.

    const { predictions } = await autocomplete.getPlacePredictions({
      ...options,
      input: query,
      componentRestrictions: {
        country: ["gb"],
      },
      types: ["geocode"],
    });
Enter fullscreen mode Exit fullscreen mode
  • geocode type instructs the Place Autocomplete service to return only geocoding results, rather than business results.

But it also includes the bigger administrative entities like cities or streets. We need users to select accurate addresses.

  • address instructs the Place Autocomplete service to return only geocoding results with a precise address.

Gotcha! That is what we need, right?... Basically, yes. However, it does not allow users to search by postcode, which is a common case for us.

So to achieve precise search results by both street names and postcodes, we wrote a custom filter. Thanks to StackOverflow for the inspiration.

export const filterPredictions = (
  results: Array<google.maps.places.AutocompletePrediction>
): Array<google.maps.places.AutocompletePrediction> =>
  results.filter(({ types }) => {
    // Keep predictions that are street addresses or postal codes
    if (types.includes("street_address") || types.includes("postal_code")) {
      return true;
    }
    // For geocode types, check for building numbers (premise/subpremise)
    if (types.includes("geocode")) {
      return types.some((type) => ["premise", "subpremise"].includes(type));
    }
    return false; // Filter out irrelevant types
  });
Enter fullscreen mode Exit fullscreen mode

If the result includes the street_address or postal_code, we assume it as the correct result.
If it has the geocode type, we check for the premise or subpremise (shortly saying, the building number or name). More about types you can read here.

The result we achieved:

AutocompleteService demo result

Step 4: Fetching Address Details with Geocoder

AutocompleteService returns only search predictions, but not the place details we need. However, with the place id and Geocoder we can get details like exact address, country, postal code and coordinates.
Geocoder was initially created to make conversions between addresses and coordinates, but it completely covers our needs.
If you need to have additional information about the place like reviews and comments, you can use the Places API.

Let's add a new method to our hook:

// Function to get detailed information about a place using its place id
const getPlaceById = async (
  placeId: string // The place_id from the AutocompleteService
): Promise<google.maps.GeocoderResult | null> => {
  const geocoder = new google.maps.Geocoder(); // Create a new instance of Geocoder

  try {
    const { results } = await geocoder.geocode({ placeId });
    // Return the first result
    return results[0];
  } catch {
     // In case of error, return null
    return null;
  }
};
Enter fullscreen mode Exit fullscreen mode

To unify the results between counties, where different administrative levels and entities exist, Google uses the address components structure. Let's check an example:

Geocoder Place keys
To format the address, we need the Baker Street 221B, NW1 6XE, London (street_number route, postal_code, locality). But, in some cases, the keys structure might differ. To cover it we made an unified serializer:

// Helper function to extract a specific address component by its type (e.g., street_address, postal_code)
export const pickAddressComponentByType = (
  result: google.maps.GeocoderResult,
  type: Array<string>
): string =>
  result.address_components.find((component) =>
    component.types.some((componentType) => type.includes(componentType))
  )?.long_name || "";

// Function to serialize the geocoded result into a structured format
export const serializeGeocoderResult = (
  result: google.maps.GeocoderResult
): TFormatedGeocoderResult => ({
  formattedAddress: result.formatted_address,
  streetAddress: pickAddressComponentByType(result, [
    "street_address",
    "premise",
    "route",
  ]),
  streetNumber: pickAddressComponentByType(result, ["street_number"]),
  city: pickAddressComponentByType(result, [
    "locality",
    "postal_town",
    "administrative_area_level_2",
  ]),
  country: pickAddressComponentByType(result, ["country"]),
  state: pickAddressComponentByType(result, ["administrative_area_level_1"]),
  postalCode: pickAddressComponentByType(result, ["postal_code"]),
  latitude: result.geometry.location.lat(),
  longitude: result.geometry.location.lng(),
});
Enter fullscreen mode Exit fullscreen mode

Notice, that we verified it only for Great Britain. So maybe you might need to enhance it for your specific cases.

Improvements for Google Maps Autocomplete

  1. Add the input debounce and minimum characters restriction to reduce the number of requests. Debouncing ensures that the API is only called after a certain delay, helping to reduce unnecessary requests as the user types. You can check these improvements in DEMO Sandbox.
  2. If you're open in your budget, you can fetch all of the places by their id and show users the options in the format you need, but not in the Google Autocomplete format.

Conclusions: Smart Address Search

In this article, I have shown how we can use different Google Maps API's to build address fields, that can replace full address forms and drastically reduce the time users need to spend on it, reducing the number of mistakes on the users' side.

Check out the DEMO sandbox, but don’t forget to set your own GOOGLE_MAPS_API_KEY.

Feel free to send your questions and suggestions in comments. I'll be glad for any conversations🌊🙌.

💖 💪 🙅 🚩
domanskyi
Valentyn Domanskyi

Posted on October 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related