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).
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,
};
};
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 [];
}
};
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>
);
}
From the predictions, we are interested in the 3 fields:
- Place full name.
- Place id.
- 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"],
},
});
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"],
});
-
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
});
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:
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;
}
};
To unify the results between counties, where different administrative levels and entities exist, Google uses the address components structure. Let's check an example:
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(),
});
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
- 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.
- 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🌊🙌.
Top comments (0)