Checkout Address Autocomplete Without a Plugin

Address Search

Learn how to create an address search with autocomplete for your WooCommerce checkout page, using the Google Places API (autocomplete).

We’ll insert a custom text box into the billing address form, straight after the country chooser, so your customers can search for their billing address. Address-matches will show as a pop-up pick-list.


The customer will choose their billing country from the standard WooCommerce drop-down/search. The chosen country will constrain the address auto-complete, so the results are more relevant. There’s no point in searching USA addresses when the customer has told you they’re in France.

After a customer has searched for their address, pressing Enter should auto-populate the billing address fields. It should NOT submit the checkout form.

The customer needs to be able to bypass the address-search to type in their address manually, if they want to.

importantMake sure you’re using a custom child theme so you can edit your functions.php file.

importantYou will need a Google JavaScript Maps/Places API Key. If your site has an interactive Google Map on it, you’ve probably already got an API Key you can use for this project.

How It’s Going to Work

When WooCommerce renders the checkout page, it creates an array of all the fields it wants to display (in PHP). Then it loops through this array and renders the HTML. We can hook into this process and inject our own fields into the array. We’ll use the woocommerce_checkout_fields filter to do this.

Here’s what the array of checkout fields looks like (expressed as JSON):

	"billing": {
		"billing_country": {
			"type": "country",
			"label": "Country/Region",
			"required": true,
			"class": [ "form-row-wide", "address-field", "update_totals_on_change" ],
			"autocomplete": "country",
			"priority": 40
		"billing_address_1": {
			"label": "Street address",
			"placeholder": "House number and street name",
			"required": true,
			"class": [ "form-row-wide", "address-field" ],
			"autocomplete": "address-line1",
			"priority": 50
	"shipping": {
	"account": [],
	"order": {
		"order_comments": {
			"type": "textarea",
			"class": [ "notes" ],
			"label": "Order notes",
			"placeholder": "Notes about your order, e.g. special notes for delivery."

So we just need to create a new billing field that’s similar to billing_address_1 (a regular text field), and we want to inject it into the array straight after the billing_country field. Notice that billing_country has a priority of 40, and billing_address_1 has a priority of 50 – this is how WooCommerce sorts the fields. So we can create our billing_search field with a priority of 45… easy.

After we’ve got the billing_search field injected into the checkout HTML, we’ll create some JavaScript magic to link up to Google and turn billing_search into an auto-complete box. We don’t want to call the Google Places JavaScript from every page of our site, because that would drag down our page-load speed, so we’ll only add it on the checkout page. Makes sense.

Let’s write some code!

The PHP Code

In your custom child theme, create a new folder called “wpt-address-autocomplete”. In this folder, create two empty files, called “address-autocomplete.css” and “address-autocomplete.js” – these are our frontend asset files, and you can leave them empty for now. Now go back to your custom child theme’s main folder and create a file called “wpt-address-autocomplete.php” and paste the following into it:


 * WP Tutorials Address Autocomplete (WPTAAC)

defined('WPINC') || die();

const WPTAAC_GOOGLE_PLACES_API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // << REPLACE THIS

function wptaac_enqueue_assets() {
	global $wptaac_have_assets_been_enqueued;

	if (is_null($wptaac_have_assets_been_enqueued)) {
		$handle = 'wptaac';
		$base_uri = get_stylesheet_directory_uri();
		$theme_version = wp_get_theme()->get('Version');

		// REF:
		$google_script_url = sprintf(

		// This is our main frontend JavaScript asset, with all our frontend code.
			$base_uri . '/wpt-address-autocomplete/address-autocomplete.js',
			['strategy' => 'defer', 'in_footer' => true]

		// Pass the Google Script URL and a default/fallback country code to the
		// frontend script in a global JavaScript variable called wptaacData.
		// We pick this up in our JavaSrtipt main entry point.
				'googleScriptUrl' => $google_script_url,
				'defaultCountry' => WPTAAC_DEFAULT_COUTNRY_CODE,

		// Add our styles.
			$base_uri . '/wpt-address-autocomplete/address-autocomplete.css',

		$wptaac_have_assets_been_enqueued = true;

function custom_woocommerce_checkout_fields($fields) {
	if (is_admin() || !is_checkout() || wp_doing_ajax()) {
		// ...
	} elseif (!array_key_exists('billing', $fields)) {
		// ...
	} elseif (!array_key_exists('billing_country', $fields['billing'])) {
		// Billing country isn't in the list of checkout fields. Weird.
	} else {

		// Inject the #billing_search field.
		$fields['billing']['billing_search'] = array(
			'type' => 'text',
			'label' => __('Search For Your Address', 'wp-tutorials'),
			'placeholder' => __('Type to search for address', 'wp-tutorials'),

	return $fields;
add_filter('woocommerce_checkout_fields', 'custom_woocommerce_checkout_fields');

This bit of back-end PHP code should be easy enough to follow… we’ve got two functions in there. One is to enqueue the frontend assets, and the other is to inject our autocomplete/search text box into the billing fields array.

There are some interesting things to note in wptaac_enqueue_assets():

  • You will need to set WPTAAC_GOOGLE_PLACES_API_KEY to your own Google Places API Key.
  • You can also set your fallback/default country code in WPTAAC_DEFAULT_COUTNRY_CODE, which is a two-character ISO country code, like GB, IN, US, DE, etc.
  • We’re using wp_localize_script() to pass some data into our frontend script, in a global JavaScript variable called wptaacData.

Now we need to we need to reference the new code from our child theme, so edit your child theme’s functions.php and add this into it:

// WooCommerce checkout Address autocomplete
require_once dirname(__FILE__) . '/wpt-address-autocomplete.php';

A Bit of Style

In your wpt-address-autocomplete folder, edit address-autocomplete.css and paste the following into it:

 * WP Tutorials Address Autocomplete (WPTAAC)
label[for="billing_search"]::before {
	font-family: 'Font Awesome 5 Free';
	font-weight: 900;
	content: '\f002';
	margin-right: 0.5em;

This snippet adds a small magnifying glass next to the billing_search label. It depends on Font Awesome 5 being available, but you can style #billing_search however you like.

That should be enough to get our custom field injected into the checkout form. Save everything, put something into your cart/basket and go to the checkout… You should see the new #billing_search field in there.

WooCommerce checkout address search
Our WooCommerce address search field in the checkout

The JavaScript Code

In your custom child theme, go into the wpt-address-autocomplete folder, edit “address-autocomplete.js” and paste the following lump into it.

 * WP Tutorials Address Autocomplete (WPTAAC)

 * Google creates a widget... which is not a WordPress widget. It's a generic
 * term for a "thing". In this case, it's the thing that links our on-screen
 * text box to the Google Places API.
var addressAutocompleteWidget = null;

 * Main entry point. Instantiate the Google Places API script and listen for
 * changes of #billing_country
(function($) {
	'use strict';
	$(window).on('load', function() {
		// console.log(`WPTAAC: Load`);

		const searchElement = $('#billing_search');
		if (searchElement.length == 0) {
			console.log('Missing element: #billing_search');
		} else if (typeof wptaacData === 'undefined') {
			// Don't do anything unless wptaacData has been passed from the backend
			// using wp_localize_script().
		} else {
			// Keep track of the last time we changed the billing country, so we
			// know if the user has selected a new country, or chosen the same
			// country again.
			wptaacData.lastSelectedCountry = null;

			// Disable the address-earch box until the Google scripts have loaded.
			$(searchElement).prop('disabled', true);

			// Uncomment this to verify that your Google Places API Key is coming
			// through into the script properly.
			// console.log(`WPTAAC: Init: ${wptaacData.googleScriptUrl}`);

			// Create a new script element, set the Google Places API URL,
			// with the callback set to our createAddressAutocomplete() function.
			var googleScriptElement = $('<script async />');
			$(googleScriptElement).prop('src', `${wptaacData.googleScriptUrl}&callback=createAddressAutocomplete`);

			// WooCommerce triggers the "updated_checkout" event when the customer
			// changes things like the billing country. We use this to change the
			// scope/restrictions for our autocomplete boc, to make the suggested
			// addresses more relevant for the customer.
			$(document.body).on('updated_checkout', function(event) {

 * This is called by the Google script, after it's loaded. This is where we
 * initialise and create Google's autocomplte widget, based on our
 * #billing_search INPUT control.
function createAddressAutocomplete() {
	const searchElement = document.getElementById('billing_search');

	if (addressAutocompleteWidget) {
		console.log('addressAutocompleteWidget has already been created!');
	} else if (!searchElement) {
		console.log('Missing element: #billing_search');
	} else {
		// Prevent the Enter key from causing a form-submission.
		google.maps.event.addDomListener(searchElement, 'keydown', function(event) {
			if (event.keyCode === 13) {

		// Create the Options for the Google Places Autocomplete widget.
		// fields: ['address_components', 'geometry', 'icon', 'name'],
		let defaultCountryCode = document.getElementById('billing_country').value;
		if (!defaultCountryCode) {
			defaultCountryCode = wptaacData.defaultCountry;

		const options = {
			componentRestrictions: {
				country: defaultCountryCode
			fields: ['address_components'],
			strictBounds: false,
			types: ['address'],

		// Create the Google Places Autocomplete widget.
		addressAutocompleteWidget = new google.maps.places.Autocomplete(

		// When the Autocomplete widget raises the place_changed event,
		// call our populateAddressFromAutocomplete() function.
		addressAutocompleteWidget.addListener("place_changed", populateAddressFromAutocomplete);

		// Enable the billing_search input field.
		searchElement.disabled = false;


function setAddressLookupCountryRestriction() {
	var newCountryCode = document.getElementById('billing_country').value;

	// Fallback to our default country code if newCountryCode is blank.
	if (!newCountryCode) {
		newCountryCode = wptaacData.defaultCountry;

	// If the country code has changed, set the country restriction on the
	// autocomplete widget and set the input focus to the billing_search
	// text box, so the user can start typing an address.
	if (addressAutocompleteWidget && (wptaacData.lastSelectedCountry != newCountryCode)) {
		// console.log(`Setting country restriction: ${newCountryCode}`);

		wptaacData.lastSelectedCountry = newCountryCode;
		addressAutocompleteWidget.setComponentRestrictions({ country: newCountryCode });


 * Get the current selected billing country code, and fall back to our default
 * country code if, for some reason, it's blank.
function getAddressAutocompleteCountryCode() {
	var countryCode = null;

	const countryCodeElement = document.getElementById('billing_country');
	if (countryCodeElement) {
		countryCode = countryCodeElement.value;

	if (!countryCode) {
		countryCode = wptaacData.defaultCountry;

	return countryCode;

 * Set all the billing fields to empty strings.
function resetBillingAddress() {
	const billingFields = [

	billingFields.forEach(function(billingField) {
		var input = document.getElementById(billingField);
		if (input) {
			input.value = '';


 * Scan a Google "place" object for a specific component
 * and return either its short_name or long_name.
function getAddressComponent(place, componentType, componentLength) {
	// const place = addressAutocompleteWidget.getPlace();
	if (!componentLength) {
		componentLength = 'short_name';

	var componentValue = null;
	for (const component of place.address_components) {
		if (component.types[0] == componentType) {
			componentValue = component[componentLength];

	return componentValue;

 * Scan the various post code components of a Google place object, pick the
 * first one that has something in it and set the content os #billing_postcode
function setBestPostalCode(place) {
	const postalComponents = [

	var input = document.getElementById('billing_postcode');
	if (input) {
		postalComponents.forEach(function(postalComponent) {
			var autoCompleteValue = getAddressComponent(place, postalComponent);
			if (input.value) {
				// There's already something in the post code box.
			} else if (!autoCompleteValue) {
				// Auto-complete has no suggestion for this field.
			} else {
				input.value = autoCompleteValue;

 * See if the state SELECT control has our state by value. If not, see if we can
 * match against an OPTION's label instead.
function setState(state) {
	const selectElement = document.getElementById('billing_state');

	// Try setting the value of the main SELECT element first.
	selectElement.value = state;

	if (state && !selectElement.value) {

		// console.log('Failed to set billing state. Trying to match option by text instead');

		Array.from(selectElement.options).forEach((optionElement) => {
			if (optionElement.text && (optionElement.text == state)) {

				// console.log(`Found: ${state} => ${optionElement.value}`);
				selectElement.value = optionElement.value;

 * This is our main handler, which is called each time the user picks an
 * autocomplete suggested address.
function populateAddressFromAutocomplete() {
	// Get the suggested "place" object from the widget.
	const place = addressAutocompleteWidget.getPlace();

	// Uncomment this to see what's in the Google place object.
	// console.log(place);
	// console.log(place.address_components);

	// Set the billing address fields values to empty.

	var billingCountryCode = getAddressAutocompleteCountryCode();
	var sublocality = null;

	if (!place) {
		console.log('Invalid / missing place from Google API');
	} else if (billingCountryCode == 'US') {
		// Special address handling for the USA.
		document.getElementById('billing_address_1').value = getAddressComponent(place, 'street_number') + ' ' + getAddressComponent(place, 'route', 'long_name');
		document.getElementById('billing_city').value = getAddressComponent(place, 'locality');
		document.getElementById('billing_postcode').value = getAddressComponent(place, 'postal_code');
		document.getElementById('billing_state').value = getAddressComponent(place, 'administrative_area_level_1');
	} else if (billingCountryCode == 'GB') {
		// Special address handling for the United Kingdom (Great Britain).
		document.getElementById('billing_address_1').value = getAddressComponent(place, 'street_number') + ' ' + getAddressComponent(place, 'route', 'long_name');
		document.getElementById('billing_city').value = getAddressComponent(place, 'locality');
		if (!document.getElementById('billing_city').value) {
			document.getElementById('billing_city').value = getAddressComponent(place, 'postal_town');

		document.getElementById('billing_state').value = getAddressComponent(place, 'administrative_area_level_2', 'long_name');

	} else {
		for (const component of place.address_components) {
			const componentType = component.types[0];

			switch (componentType) {
				case "street_number":
					document.getElementById('billing_address_1').value = component.long_name;

				case "route":
					document.getElementById('billing_address_1').value += ' ' + component.short_name;

				case "sublocality_level_1":
					sublocality = component.long_name;

				case "locality":
					document.getElementById('billing_address_2').value = component.long_name;
					document.getElementById('billing_city').value = component.long_name;

				case "postal_town":
					document.getElementById('billing_city').value = component.long_name;

				case "administrative_area_level_1":

				case "country":
					// Either an unhandled or unnecessary address component.
					// console.log(componentType);
					// console.log(component);


	// If Address line 2 and Billing City are the same, maybe we can pull
	// something meaningful from sublocality_level_1.
	const billingLine2Element = document.getElementById('billing_address_2');
	const billingCityElement = document.getElementById('billing_city');
	if (billingLine2Element.value && (billingLine2Element.value == billingCityElement.value)) {
		if (sublocality) {
			billingLine2Element.value = sublocality;
		} else {
			billingLine2Element.value = '';

	// If billing state is empty, try pulling it from administrative_area_level_2 instead.
	if (!document.getElementById('billing_state').value.length) {
		document.getElementById('billing_state').value = getAddressComponent(place, 'administrative_area_level_2');

	// Trigger a change on the #billing_state field so that the on-screen
	// state chooser gets updated.
	if (document.getElementById('billing_state').value) {

	// Because we've changed a bunch of the address fields, we need to trigger
	// a re-validate of the form fields.
	jQuery('.validate-required input, .validate-required select').trigger('validate');

There’s quite a bit to unpick in here, but it’s thoroughly commented and it should be easy enough to see what each chunk of code does. The main points of note are:

  • The top jQuery section is our “Main entry point“. This is the first lump of code that gets executed, and it’ll only do something if it detects the global JavaScript variable wptaacData, which contains parameters such as our Google Script URL (which contains the API Key). This code creates a new script element and attaches it to the document’s head element. When the script has finished loading, it calls our createAddressAutocomplete() function, which is where we start setting things up properly.
  • The createAddressAutocomplete() function uses the Google code to create a new “widget”, which is NOT a WordPress widget. It’s just a “thing” that links our on-screen #billing_search field to Google’s API. This code also tells the Google widget/thing to call populateAddressFromAutocomplete() when the user selects an address from the pick-list.
  • In populateAddressFromAutocomplete() is where we take the results of the autocomplete and poke the values into the billing fields. Note that we’ve got a bit of code in there to do specific things for the US and GB countries. We might find that lots of other countries benefit from their own specific-case handlers too.
  • The only other things in here are helper functions. I’ve given them meaningful names, so you can figure out what they do easily enough.

Test Test Test

This code runs at a critical point in a website’s primary workflow. You’re capturing user data immediately before a financial transaction. If this script makes the user experience bad in any way, or if it clashes with WooCommerce in the future (because of some unforeseen update to WooCommerce) then you’ll need to know about it. So…

Test the code… Test it a lot.

  • Try lots of different addresses, for different countries.
  • Make sure you’ve got a quick way of disabling the auto-complete code completely (just comment-out the call to require_once() in functions.php).
  • Try it on mobile devices, especially devices with small screens.
  • Test with different character sets, such as Japanese or Cyrillic.
  • Make sure your Google places API Key is restricted to your website’s HTTP Referrer, otherwise somebody might “steal” your API key and you’ll end up with a big bill from Google for a bunch of API calls you didn’t actually make.

That’s it. Happy addressing! 😎 👍

Featured plugin

Address Autocomplete for WordPress and WooCommerce
Use the Google Places API to autocomplete billing and shipping postal addresses. Works with WooCommerce and other plugins. Developer friendly and extendable.
Address Autocomplete for WooCommerce

Like This Tutorial?

Let us know

WordPress plugins for developers

21 thoughts on “Checkout Address Autocomplete Without a Plugin”

  1. Greetings,
    When I’m selecting US Country it’s adding “null” automatically to my street address as same it is in demo…How I can solve this and why this is coming
    Thank you for useful article
    waiting for your response!

    • Hello! The demo seems to work OK here. If you select “UK” and type “123” into Find address, does it come back with “123 Buckingham Palace Road”? Are you seeing any errors in your browser’s JavaScript console?

      • I found what is causing this error. If user does not enter any number in the search field then the output is incomplete/corrupted, half of postal code and/or null prefix. Any way to require number in that field to generate any output? Thanks! Your work is incredible.

  2. Hi there – I’m wondering how we’d be able to adjust this code to work with New Zealand addresses. The State field seems to autofill for most other countries but not with New Zealand addresses. I think it must be something to do with our ‘regions’ being in an unusual format.

    • There are probably several countries that will need special code tweaks to handle specific address formats. I’ll have a look at some NZ addresses later today and see if we can make a modification to the JS for you.

    • I’ve found the problem. The Google place lookup returns the NZ states as full text, like “Waikato”, “Taranaki”, etc. But the WooCommerce state chooser drop-down list (SELECT element) has values of “WKO”, “TKI”, etc.

      I’ve updated the JS code with a new function called setState(). It first tries to set the state based on the SELECT’s value (which works for USA and most other places). If it fails to find a valid value, it loops through the state OPTIONs looking for a full-text match instead.

      It looks OK in my tests here. Try it at your end and let me know if it fixes it for your customers too.

      • That worked perfectly! One more question if you don’t mind, is how we could include the sublocality (town / suburb) in either ‘billing_address_2’ field, or comma separated in the ‘billing_city’ field? Eg. In the city field “Henderson, Auckland”. Thanks very much for your help getting the State / Region field to populate correctly.

        • Hi Josh

          I’ve made another minor modification to the JavaScript code for you to try and use the “administrative_area_level_2” address component, if it’s available. I think that should do the job for you.

          Try the new JS at your end and let me know if that’s good.

          • Looks like that’s done the trick! Thanks very much for your help with this and supplying the code for everyone. After testing several plugins, none of them worked very well, but this code works great. Such a useful feature for end users.

          • Sorry to bother you again! I seem to be struggling somewhat with adapting the code to include a search field for the shipping address also. Would this be something that is relatively easy to implement or a bit more involved?

          • There’s a couple of hours work involved in doing that. It’s perfectly do-able, but it’s not as simple as copy-and-paste with a bit of search-and-replace. The JS code (and some of the PHP code) would need to be restructured a bit to support multiple address lookup widgets on the same page. I’d consider implementing it as a bespoke paid project for you. Ping me an email if that’s something you’d be interested in.

  3. I’m having an issue that no matter what I try (default theme, plugins disabled), the code does not refresh shipping options. Do you have a suggestion what may be the culprit?

    • Usually this is caused by using either a Page Builder, or a theme that adjusts how the checkout JavaScript works (like Astra Theme’s “modern” checkout). If you’re using software that changes how the checkout page JavaScript works, things will break.

      But… if you’re using default theme and other plugins are disabled… it should work fine. The first thing to check is the JavaScript console in your browser’s Dev Tools. If there are any errors in there, we need to fix those first. But if there are no errors in there, install the “Query Monitor” plugin and go the checkout again. It will help you find any problems/bugs.

      The code is quite robust and forms the core of our commercial plugin on, so we’re not tracking any issues with it. If you want me to look at your dev site, send me a DM from the contact form and I can have a look for you.

      • My theme support found the solution. I quote:

        const defaultCountryCode = document.getElementById(‘billing_country’).value;

        let defaultCountryCode = document.getElementById(‘billing_country’).value;

        Using const is not valid if you plan on changing the variable.

        Also, change wp_enqueue_script function to:

        $base_uri . ‘/wpt-address-autocomplete/address-autocomplete.js’,
        [ ‘strategy’ => ‘defer’, ‘in_footer’ => true ]
        This makes sure your script is “deferred”, it does not block the page rendering process (thus improves your website’s performance).

  4. Hello I have tried in many attempts but I get a fatal error:
    Fatal error
    : Uncaught Error: Undefined constant “‘strategy’” in /home/ Stack trace: #0 /home/ wptaac_enqueue_assets() #1 /home/ custom_woocommerce_checkout_fields() #2 /home/ WP_Hook->apply_filters() #3 /home/ apply_filters() #4 /home/ WC_Checkout->get_checkout_fields() #5 /home/ include(‘…’) #6 /home/ wc_get_template() #7 /home/ WC_Shortcode_Checkout::checkout() #8 /home/ WC_Shortcode_Checkout::output() #9 /home/ WC_Shortcodes::shortcode_wrapper() #10 /home/ WC_Shortcodes::checkout() #11 [internal function]: do_shortcode_tag() #12 /home/ preg_replace_callback() #13 /home/ do_shortcode() #14 /home/ WP_Hook->apply_filters() #15 /home/ apply_filters() #16 /home/ woodmart_get_the_content() #17 /home/ include(‘…’) #18 /home/ require_once(‘…’) #19 /home/ require(‘…’) #20 {main} thrown in
    on line

    I am not sure if is my theme problem but I can’t make it work.

    I hope you can sugget me something to solve it.

    Best Regards

    • Oh dear. It looks like a copy-and-paste error my my part. The wrong type of apostrophe character went into the code on line 29, which caused the syntax error. Sorry about that.

      I’ve updated the tutorial now, so if you replace your PHP with the new PHP, it should work correctly.

    • Ah yes, the Blocks Checkout. It’s high on my TODO list. I’ve made a commercial version of this tutorial with improved support for different country address formats, and some other WooCommerce bits – We’re going to add Block Checkout support to plugin.

      I suspect I’ll write a tutorial on how to add stuff to the Blocks Checkout too, but it will be later in the year. I need to figure out how to make it work, first 🙂


Leave a comment