(AUTOMATIC) opensource update (#125)

diff --git a/README.md b/README.md
index 4c932a1..535172d 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,16 @@
 
 The C++ library (in very portable C++03) of _libaddressinput_ is used in address-related
 projects in [Chromium](http://www.chromium.org/Home).
+[requestAutocomplete()](http://www.html5rocks.com/en/tutorials/forms/requestautocomplete/)
+in [Chromium](http://www.chromium.org/Home). The source code for that is a good
+example of how to use this library to implement a complex feature in a real
+application:
 
+https://src.chromium.org/viewvc/chrome/trunk/src/third_party/libaddressinput/
 https://chromium.googlesource.com/chromium/src/+/master/third_party/libaddressinput/
 
+Video: [Easy International Checkout with Chrome](https://www.youtube.com/watch?v=ljYeHwGgzQk)
+
 ## Java
 
 The Java library of _libaddressinput_ is written for use in
diff --git a/android/README b/android/README
deleted file mode 100644
index b044bd0..0000000
--- a/android/README
+++ /dev/null
@@ -1,45 +0,0 @@
-Building and running tests with Android
-=======================================
-
-The easiest way to build libaddressinput for Android and run all the tests is
-using the Gradle project automation tool:
-
-http://tools.android.com/tech-docs/new-build-system
-http://www.gradle.org/
-
-
-Prerequisite dependencies for using Gradle with Android
--------------------------------------------------------
-Android Studio: https://developer.android.com/sdk/index.html
-or
-Android SDK Tools: https://developer.android.com/sdk/index.html#Other
-
-Set the ANDROID_HOME environment variable to the root of the SDK.
-
-Install the following packages:
-* Tools/Android SDK Build-tools (Rev. 21.1.2)
-* Android 5.1 (API 22)
-* Extras/Android Support Library
-
-Gradle (latest version):
-  https://services.gradle.org/distributions/gradle-2.3-bin.zip
-
-Note: Additionally you must take care to avoid having multiple versions of
-Gradle on your path, as this can cause problems.
-
-
-Building and Running
---------------------
-After installing all the prerequisites, check that everything is working by
-running:
-
-$ gradle build
-
-With an Android emulator running or an Android device connected, the following
-command line then builds the library and runs the tests:
-
-$ gradle connectedAndroidTest
-
-The test runner logs to the system log, which can be viewed using logcat:
-
-$ adb logcat
diff --git a/android/README.md b/android/README.md
new file mode 100644
index 0000000..03af760
--- /dev/null
+++ b/android/README.md
@@ -0,0 +1,134 @@
+# Building and running tests with Android
+
+
+The easiest way to build libaddressinput for Android and run all the tests is
+using the Gradle project automation tool:
+
+http://tools.android.com/tech-docs/new-build-system
+http://www.gradle.org/
+
+
+## Prerequisite dependencies for using Gradle with Android
+
+Android Studio: https://developer.android.com/sdk/index.html
+or
+Android SDK Tools: https://developer.android.com/sdk/index.html#Other
+
+Set the ANDROID_HOME environment variable to the root of the SDK.
+
+Install the following packages:
+* Tools/Android SDK Build-tools (Rev. 21.1.2)
+* Android 5.1 (API 22)
+* Extras/Android Support Library
+
+Gradle (latest version):
+  https://services.gradle.org/distributions/gradle-2.3-bin.zip
+
+Note: Additionally you must take care to avoid having multiple versions of
+Gradle on your path, as this can cause problems.
+
+
+## Building and Running
+
+After installing all the prerequisites, check that everything is working by
+running:
+
+$ gradle build
+
+With an Android emulator running or an Android device connected, the following
+command line then builds the library and runs the tests:
+
+$ gradle connectedAndroidTest
+
+The test runner logs to the system log, which can be viewed using logcat:
+
+$ adb logcat
+
+# Integrating with Android Apps
+
+
+1. Clone libaddressinput from Github or download and unzip to a folder called 'libaddressinput'.
+
+
+2. From a terminal window, change into the folder: `cd libaddressinput/`
+
+3. Build the widget and library via gradle:
+
+    `gradle build`
+
+4. Copy the widget and the common libraries:
+
+    `cp android/build/outputs/aar/android-release.aar path/to/project/app/libs/`
+
+    `cp common/build/libs/common.jar path/to/project/app/libs/`
+
+    Note: Be sure top replace 'path/to/project' with the name of your project.
+
+5. Import both modules into your app.
+
+    Note: If you use Android Studio, check out the [user guide](https://developer.android.com/studio/projects/android-library.html#AddDependency) and follow the instructions under 'Add your library as a dependency'. Be sure to add *both* modules as dependencies of the app.
+
+6. Add the widget to your app. Note: This Assumes a default empty project configuration:
+
+    i. In activity_main.xml add:
+
+    ```
+    <LinearLayout
+        android:id="@+id/addresswidget"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"/>
+     ```
+
+    ii. In MainActivity.java add the following import statements:
+
+    ```
+    import android.view.ViewGroup;
+
+    import com.android.i18n.addressinput.AddressWidget;
+    import com.google.i18n.addressinput.common.FormOptions;
+    import com.google.i18n.addressinput.common.ClientCacheManager;
+    import com.google.i18n.addressinput.common.SimpleClientCacheManager;
+    ```
+
+    iii. Define the widget on the object scope
+
+        `private AddressWidget addressWidget;`
+
+    iv. Add the widget to the ViewGroup
+    ```
+    ViewGroup viewGroup = (ViewGroup) findViewById(R.id.addresswidget);
+    FormOptions defaultFormOptions = new FormOptions();
+    ClientCacheManager cacheManager = new SimpleClientCacheManager();
+    this.addressWidget = new AddressWidget(this, viewGroup, defaultFormOptions, cacheManager);
+    ```
+
+Example:
+
+```
+package com.example.google.widgetdemo;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.ViewGroup;
+
+import com.android.i18n.addressinput.AddressWidget;
+import com.google.i18n.addressinput.common.FormOptions;
+import com.google.i18n.addressinput.common.ClientCacheManager;
+import com.google.i18n.addressinput.common.SimpleClientCacheManager;
+
+public class MainActivity extends AppCompatActivity {
+    private AddressWidget addressWidget;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        ViewGroup viewGroup = (ViewGroup) findViewById(R.id.addresswidget);
+        FormOptions defaultFormOptions = new FormOptions();
+        ClientCacheManager cacheManager = new SimpleClientCacheManager();
+        this.addressWidget = new AddressWidget(this, viewGroup, defaultFormOptions, cacheManager);
+    }
+}
+```
diff --git a/android/build.gradle b/android/build.gradle
index 0d48fc8..a25bd9d 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -32,6 +32,8 @@
 
 dependencies {
     compile project(':common')
+    compile 'com.google.android.gms:play-services-location:10.0.0'
+    compile 'com.google.android.gms:play-services-places:9.2.0'
 }
 
 android {
@@ -56,5 +58,9 @@
      */
     compileSdkVersion 22
     buildToolsVersion '21.1.2'
+    defaultConfig {
+        minSdkVersion 17
+        targetSdkVersion 22
+    }
 }
 
diff --git a/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java
new file mode 100644
index 0000000..d7898f1
--- /dev/null
+++ b/android/src/androidTest/java/com/android/i18n/addressinput/AddressAutocompleteControllerTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.i18n.addressinput;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.AutoCompleteTextView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.android.i18n.addressinput.AddressAutocompleteController.AddressAdapter;
+import com.android.i18n.addressinput.AddressAutocompleteController.AddressPrediction;
+import com.android.i18n.addressinput.testing.TestActivity;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.i18n.addressinput.common.AddressAutocompleteApi;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import com.google.i18n.addressinput.common.AddressData;
+import com.google.i18n.addressinput.common.OnAddressSelectedListener;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link AddressAutocompleteController}. */
+public class AddressAutocompleteControllerTest
+    extends ActivityInstrumentationTestCase2<TestActivity> {
+  private static final String TEST_QUERY = "TEST_QUERY";
+
+  private Context context;
+
+  private AddressAutocompleteController controller;
+  private AutoCompleteTextView textView;
+
+  // Mock services
+  private @Mock AddressAutocompleteApi autocompleteApi;
+  private @Mock PlaceDetailsApi placeDetailsApi;
+
+  // Mock data
+  private @Captor ArgumentCaptor<FutureCallback<List<? extends AddressAutocompletePrediction>>>
+      autocompleteCallback;
+  private @Mock AddressAutocompletePrediction autocompletePrediction;
+
+  public AddressAutocompleteControllerTest() {
+    super(TestActivity.class);
+  }
+
+  @Override
+  protected void setUp() {
+    MockitoAnnotations.initMocks(this);
+
+    context = getActivity();
+
+    textView = new AutoCompleteTextView(context);
+    controller =
+        new AddressAutocompleteController(context, autocompleteApi, placeDetailsApi)
+            .setView(textView);
+  }
+
+  // Tests for the AddressAutocompleteController
+
+  public void testAddressAutocompleteController() throws InterruptedException, ExecutionException {
+    final AddressData expectedAddress = AddressData.builder()
+        .addAddressLine("1600 Amphitheatre Parkway")
+        .setLocality("Mountain View")
+        .setAdminArea("California")
+        .setCountry("US")
+        .build();
+
+    Future<AddressData> actualAddress = getAutocompletePredictions(expectedAddress);
+
+    assertEquals(1, textView.getAdapter().getCount());
+    assertEquals(actualAddress.get(), expectedAddress);
+  }
+
+  // Tests for the AddressAdapter
+
+  public void testAddressAdapter_getItem() {
+    AddressAdapter adapter = new AddressAdapter(context);
+    List<AddressPrediction> predictions =
+        Lists.newArrayList(new AddressPrediction(TEST_QUERY, autocompletePrediction));
+
+    adapter.refresh(predictions);
+    assertEquals(adapter.getCount(), predictions.size());
+    for (int i = 0; i < predictions.size(); i++) {
+      assertEquals("Item #" + i, predictions.get(0), adapter.getItem(0));
+    }
+  }
+
+  public void testAddressAdapter_getView() {
+    AddressAdapter adapter = new AddressAdapter(context);
+    List<AddressPrediction> predictions =
+        Lists.newArrayList(new AddressPrediction(TEST_QUERY, autocompletePrediction));
+
+    adapter.refresh(predictions);
+    for (int i = 0; i < predictions.size(); i++) {
+      assertNotNull("Item #" + i, adapter.getView(0, null, new LinearLayout(context)));
+    }
+  }
+
+  // Helper functions
+
+  private Future<AddressData> getAutocompletePredictions(AddressData expectedAddress) {
+    // Set up the AddressData to be returned from the AddressAutocompleteApi and PlaceDetailsApi.
+    when(autocompleteApi.isConfiguredCorrectly()).thenReturn(true);
+    when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID");
+    when(placeDetailsApi.getAddressData(autocompletePrediction))
+        .thenReturn(Futures.immediateFuture(expectedAddress));
+
+    // Perform a click on the first autocomplete suggestion once it is loaded.
+    textView
+        .getAdapter()
+        .registerDataSetObserver(
+            new DataSetObserver() {
+              @Override
+              public void onInvalidated() {}
+
+              @Override
+              public void onChanged() {
+                // For some reason, performing a click on the view or dropdown view associated with
+                // the first item in the list doesn't trigger the onItemClick listener in tests, so
+                // we trigger it manually here.
+                textView
+                    .getOnItemClickListener()
+                    .onItemClick(new ListView(context), new TextView(context), 0, 0);
+              }
+            });
+
+    // The OnAddressSelectedListener is the way for the AddressWidget to consume the AddressData
+    // produced by autocompletion.
+    final SettableFuture<AddressData> result = SettableFuture.create();
+    controller.setOnAddressSelectedListener(
+        new OnAddressSelectedListener() {
+          @Override
+          public void onAddressSelected(AddressData address) {
+            result.set(address);
+          }
+        });
+
+    // Actually trigger the behaviors mocked above.
+    textView.setText(TEST_QUERY);
+
+    verify(autocompleteApi)
+        .getAutocompletePredictions(any(String.class), autocompleteCallback.capture());
+    autocompleteCallback.getValue().onSuccess(Lists.newArrayList(autocompletePrediction));
+
+    return result;
+  }
+}
diff --git a/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncRequestApiTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java
similarity index 96%
rename from android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncRequestApiTest.java
rename to android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java
index d9f0796..7b558b1 100644
--- a/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncRequestApiTest.java
+++ b/android/src/androidTest/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApiTest.java
@@ -16,24 +16,22 @@
 
 package com.android.i18n.addressinput;
 
+import com.android.i18n.addressinput.testing.AsyncTestCase;
 import com.google.i18n.addressinput.common.AsyncRequestApi;
 import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback;
 import com.google.i18n.addressinput.common.JsoMap;
-
-import com.android.i18n.addressinput.testing.AsyncTestCase;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.ServerSocket;
 import java.net.Socket;
 
-public class AndroidAsyncRequestApiTest extends AsyncTestCase {
+public class AndroidAsyncEncodedRequestApiTest extends AsyncTestCase {
   private AsyncRequestApi requestApi;
 
   @Override
   public void setUp() {
-    requestApi = new AndroidAsyncRequestApi();
+    requestApi = new AndroidAsyncEncodedRequestApi();
   }
 
   public void testRequestObject() throws Exception {
diff --git a/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java
new file mode 100644
index 0000000..23f40ee
--- /dev/null
+++ b/android/src/androidTest/java/com/android/i18n/addressinput/PlaceDetailsClientTest.java
@@ -0,0 +1,87 @@
+package com.android.i18n.addressinput;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.test.ActivityInstrumentationTestCase2;
+import com.android.i18n.addressinput.testing.TestActivity;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import com.google.i18n.addressinput.common.AddressData;
+import com.google.i18n.addressinput.common.AsyncRequestApi;
+import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback;
+import com.google.i18n.addressinput.common.JsoMap;
+import java.util.concurrent.ExecutionException;
+import org.json.JSONException;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link PlaceDetailsClient}. */
+public class PlaceDetailsClientTest extends ActivityInstrumentationTestCase2<TestActivity> {
+  @Mock private AsyncRequestApi asyncRequestApi;
+  @Mock private AddressAutocompletePrediction autocompletePrediction;
+
+  @Captor ArgumentCaptor<AsyncCallback> callbackCaptor;
+
+  private PlaceDetailsClient placeDetailsClient;
+
+  public PlaceDetailsClientTest() {
+    super(TestActivity.class);
+  }
+
+  @Override
+  protected void setUp() {
+    MockitoAnnotations.initMocks(this);
+
+    placeDetailsClient = new PlaceDetailsClient("TEST_API_KEY", asyncRequestApi);
+    when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID");
+  }
+
+  public void testOnSuccess() throws InterruptedException, ExecutionException, JSONException {
+    ListenableFuture<AddressData> addressData =
+        placeDetailsClient.getAddressData(autocompletePrediction);
+
+    verify(asyncRequestApi)
+        .requestObject(any(String.class), callbackCaptor.capture(), eq(PlaceDetailsClient.TIMEOUT));
+    callbackCaptor.getValue().onSuccess(JsoMap.buildJsoMap(TEST_RESPONSE));
+
+    assertEquals(
+        AddressData.builder()
+            .setAddress("1600 Amphitheatre Parkway")
+            .setLocality("Mountain View")
+            .setAdminArea("CA")
+            .setCountry("US")
+            .setPostalCode("94043")
+            .build(),
+        addressData.get());
+  }
+
+  public void testOnFailure() {
+    ListenableFuture<AddressData> addressData =
+        placeDetailsClient.getAddressData(autocompletePrediction);
+
+    verify(asyncRequestApi)
+        .requestObject(any(String.class), callbackCaptor.capture(), eq(PlaceDetailsClient.TIMEOUT));
+    callbackCaptor.getValue().onFailure();
+
+    assertTrue(addressData.isCancelled());
+  }
+
+  private static final String TEST_RESPONSE =
+      "{"
+          + "  'result' : {"
+          + "    'adr_address' : '\\u003cspan class=\\\"street-address\\\"\\u003e1600 Amphitheatre Parkway\\u003c/span\\u003e, \\u003cspan class=\\\"locality\\\"\\u003eMountain View\\u003c/span\\u003e, \\u003cspan class=\\\"region\\\"\\u003eCA\\u003c/span\\u003e \\u003cspan class=\\\"postal-code\\\"\\u003e94043\\u003c/span\\u003e, \\u003cspan class=\\\"country-name\\\"\\u003eUSA\\u003c/span\\u003e',"
+          + "    'address_components' : ["
+          + "      {"
+          + "        'long_name' : 'United States',"
+          + "        'short_name' : 'US',"
+          + "        'types' : [ 'country', 'political' ]"
+          + "      }"
+          + "    ]"
+          + "  }"
+          + "}";
+}
diff --git a/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java b/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java
new file mode 100644
index 0000000..f0495bf
--- /dev/null
+++ b/android/src/androidTest/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImplTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.i18n.addressinput.autocomplete.gmscore;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.location.Location;
+import android.test.ActivityInstrumentationTestCase2;
+import com.android.i18n.addressinput.testing.TestActivity;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.PendingResult;
+import com.google.android.gms.common.api.PendingResults;
+import com.google.android.gms.location.FusedLocationProviderApi;
+import com.google.android.gms.location.places.AutocompleteFilter;
+import com.google.android.gms.location.places.AutocompletePrediction;
+import com.google.android.gms.location.places.AutocompletePredictionBuffer;
+import com.google.android.gms.location.places.GeoDataApi;
+import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.i18n.addressinput.common.AddressAutocompleteApi;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/** Unit tests for {@link AddressAutocompleteApi}. */
+public class AddressAutocompleteApiImplTest extends ActivityInstrumentationTestCase2<TestActivity> {
+  private static final String TAG = "AddrAutoApiTest";
+  private static final String TEST_QUERY = "TEST_QUERY";
+
+  private AddressAutocompleteApi addressAutocompleteApi;
+
+  // Mock services
+  private GeoDataApi geoDataApi = mock(GeoDataApi.class);
+  private GoogleApiClient googleApiClient = mock(GoogleApiClient.class);
+  private FusedLocationProviderApi locationApi = mock(FusedLocationProviderApi.class);
+
+  // Mock data
+  private AutocompletePredictionBuffer autocompleteResults =
+      mock(AutocompletePredictionBuffer.class);
+  private PendingResult<AutocompletePredictionBuffer> autocompletePendingResults =
+      PendingResults.immediatePendingResult(autocompleteResults);
+  private AutocompletePrediction autocompletePrediction = mock(AutocompletePrediction.class);
+
+  public AddressAutocompleteApiImplTest() {
+    super(TestActivity.class);
+  }
+
+  @Override
+  protected void setUp() {
+    addressAutocompleteApi =
+        new AddressAutocompleteApiImpl(googleApiClient, geoDataApi, locationApi);
+  }
+
+  // Tests for the AddressAutocompleteApi
+
+  public void testAddressAutocompleteApi() throws InterruptedException, ExecutionException {
+    when(googleApiClient.isConnected()).thenReturn(true);
+    when(locationApi.getLastLocation(googleApiClient)).thenReturn(new Location("TEST_PROVIDER"));
+
+    Future<List<? extends AddressAutocompletePrediction>> actualPredictions =
+        getAutocompleteSuggestions();
+
+    List<AddressAutocompletePrediction> expectedPredictions =
+        Lists.newArrayList(new AddressAutocompletePredictionImpl(autocompletePrediction));
+
+    assertEquals(actualPredictions.get(), expectedPredictions);
+  }
+
+  public void testAddressAutocompleteApi_deviceLocationMissing()
+      throws InterruptedException, ExecutionException {
+    when(googleApiClient.isConnected()).thenReturn(true);
+    when(locationApi.getLastLocation(googleApiClient)).thenReturn(null);
+
+    Future<List<? extends AddressAutocompletePrediction>> actualPredictions =
+        getAutocompleteSuggestions();
+
+    List<AddressAutocompletePrediction> expectedPredictions =
+        Lists.newArrayList(new AddressAutocompletePredictionImpl(autocompletePrediction));
+
+    assertEquals(actualPredictions.get(), expectedPredictions);
+  }
+
+  public void testAddressAutocompleteApi_isConfiguredCorrectly() {
+    when(googleApiClient.isConnected()).thenReturn(true);
+    assertTrue(addressAutocompleteApi.isConfiguredCorrectly());
+  }
+
+  // Helper functions
+
+  private Future<List<? extends AddressAutocompletePrediction>> getAutocompleteSuggestions() {
+    // Set up the AddressData to be returned from the PlaceAutocomplete API + PlaceDetailsApi.
+    // Most of the objects that are mocked here are not services, but simply data without any
+    // public constructors.
+    when(geoDataApi.getAutocompletePredictions(
+            eq(googleApiClient),
+            eq(TEST_QUERY),
+            any(LatLngBounds.class),
+            any(AutocompleteFilter.class)))
+        .thenReturn(autocompletePendingResults);
+    when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID");
+    when(autocompletePrediction.getFullText(null)).thenReturn("TEST_PREDICTION");
+    when(autocompleteResults.iterator())
+        .thenReturn(Arrays.asList(autocompletePrediction).iterator());
+    when(autocompletePrediction.getPlaceId()).thenReturn("TEST_PLACE_ID");
+    when(autocompletePrediction.getPrimaryText(null)).thenReturn("TEST_PRIMARY_ID");
+    when(autocompletePrediction.getSecondaryText(null)).thenReturn("TEST_SECONDARY_ID");
+
+    SettableFuture<List<? extends AddressAutocompletePrediction>> actualPredictions =
+        SettableFuture.create();
+    addressAutocompleteApi.getAutocompletePredictions(
+        TEST_QUERY,
+        new FutureCallback<List<? extends AddressAutocompletePrediction>>() {
+          @Override
+          public void onSuccess(List<? extends AddressAutocompletePrediction> predictions) {
+            actualPredictions.set(predictions);
+          }
+
+          @Override
+          public void onFailure(Throwable error) {
+            assertTrue("Error getting autocomplete predictions: " + error.toString(), false);
+          }
+        });
+
+    return actualPredictions;
+  }
+}
diff --git a/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java b/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java
new file mode 100644
index 0000000..c71eb4e
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressAutocompleteController.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.i18n.addressinput;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AutoCompleteTextView;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.i18n.addressinput.common.AddressAutocompleteApi;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import com.google.i18n.addressinput.common.AddressData;
+import com.google.i18n.addressinput.common.OnAddressSelectedListener;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Controller for address autocomplete results. */
+class AddressAutocompleteController {
+
+  private static final String TAG = "AddressAutocompleteCtrl";
+
+  private AddressAutocompleteApi autocompleteApi;
+  private PlaceDetailsApi placeDetailsApi;
+  private AddressAdapter adapter;
+  private OnAddressSelectedListener listener;
+
+  private TextWatcher textChangedListener =
+      new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int after) {
+          getAddressPredictions(s.toString());
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {}
+      };
+
+  private AdapterView.OnItemClickListener onItemClickListener =
+      new AdapterView.OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+          if (listener != null) {
+            AddressAutocompletePrediction prediction =
+                (AddressAutocompletePrediction)
+                    adapter.getItem(position).getAutocompletePrediction();
+
+            (new AsyncTask<AddressAutocompletePrediction, Void, AddressData>() {
+                  @Override
+                  protected AddressData doInBackground(
+                      AddressAutocompletePrediction... predictions) {
+                    try {
+                      return placeDetailsApi.getAddressData(predictions[0]).get();
+                    } catch (Exception e) {
+                      cancel(true);
+                      Log.i(TAG, "Error getting place details: ", e);
+                      return null;
+                    }
+                  }
+
+                  @Override
+                  protected void onPostExecute(AddressData addressData) {
+                    Log.e(TAG, "AddressData: " + addressData.toString());
+                    listener.onAddressSelected(addressData);
+                  }
+                })
+                .execute(prediction);
+          } else {
+            Log.i(TAG, "No onAddressSelected listener.");
+          }
+        }
+      };
+
+  AddressAutocompleteController(
+      Context context, AddressAutocompleteApi autocompleteApi, PlaceDetailsApi placeDetailsApi) {
+    this.placeDetailsApi = placeDetailsApi;
+    this.autocompleteApi = autocompleteApi;
+
+    adapter = new AddressAdapter(context);
+  }
+
+  AddressAutocompleteController setView(AutoCompleteTextView textView) {
+    textView.setAdapter(adapter);
+    textView.setOnItemClickListener(onItemClickListener);
+    textView.addTextChangedListener(textChangedListener);
+
+    return this;
+  }
+
+  AddressAutocompleteController setOnAddressSelectedListener(OnAddressSelectedListener listener) {
+    this.listener = listener;
+    return this;
+  }
+
+  void getAddressPredictions(final String query) {
+    if (!autocompleteApi.isConfiguredCorrectly()) {
+      return;
+    }
+
+    autocompleteApi.getAutocompletePredictions(
+        query,
+        new FutureCallback<List<? extends AddressAutocompletePrediction>>() {
+          @Override
+          public void onSuccess(List<? extends AddressAutocompletePrediction> predictions) {
+            List<AddressPrediction> wrappedPredictions = new ArrayList<>();
+
+            for (AddressAutocompletePrediction prediction : predictions) {
+              wrappedPredictions.add(new AddressPrediction(query, prediction));
+            }
+
+            adapter.refresh(wrappedPredictions);
+          }
+
+          @Override
+          public void onFailure(Throwable error) {
+            Log.i(TAG, "Error getting autocomplete predictions: ", error);
+          }
+        });
+  }
+
+  @VisibleForTesting
+  static class AddressPrediction {
+    private String prefix;
+    private AddressAutocompletePrediction autocompletePrediction;
+
+    AddressPrediction(String prefix, AddressAutocompletePrediction prediction) {
+      this.prefix = prefix;
+      this.autocompletePrediction = prediction;
+    }
+
+    String getPrefix() {
+      return prefix;
+    };
+
+    AddressAutocompletePrediction getAutocompletePrediction() {
+      return autocompletePrediction;
+    };
+
+    @Override
+    public final String toString() {
+      return getPrefix();
+    }
+  }
+
+  // The main purpose of this custom adapter is the custom getView function.
+  // This adapter extends BaseAdapter instead of ArrayAdapter because ArrayAdapter has a filtering
+  // bug that is triggered by the AutoCompleteTextView (see
+  // http://www.jaysoyer.com/2014/07/filtering-problems-arrayadapter/).
+  @VisibleForTesting
+  static class AddressAdapter extends BaseAdapter implements Filterable {
+    private Context context;
+
+    private List<AddressPrediction> predictions;
+
+    AddressAdapter(Context context) {
+      this.context = context;
+      this.predictions = new ArrayList<AddressPrediction>();
+    }
+
+    public AddressAdapter refresh(List<AddressPrediction> newPredictions) {
+      predictions = newPredictions;
+      notifyDataSetChanged();
+
+      return this;
+    }
+
+    @Override
+    public int getCount() {
+      return predictions.size();
+    }
+
+    @Override
+    public AddressPrediction getItem(int position) {
+      return predictions.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+      return position;
+    }
+
+    // No-op filter.
+    // Results from the PlaceAutocomplete API don't need to be filtered any further.
+    @Override
+    public Filter getFilter() {
+      return new Filter() {
+        @Override
+        public Filter.FilterResults performFiltering(CharSequence constraint) {
+          Filter.FilterResults results = new Filter.FilterResults();
+          results.count = predictions.size();
+          results.values = predictions;
+
+          return results;
+        }
+
+        @Override
+        public void publishResults(CharSequence constraint, Filter.FilterResults results) {}
+      };
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+      LayoutInflater inflater =
+          (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+      LinearLayout view =
+          convertView instanceof LinearLayout
+              ? (LinearLayout) convertView
+              : (LinearLayout)
+                  inflater.inflate(R.layout.address_autocomplete_dropdown_item, parent, false);
+      AddressPrediction prediction = predictions.get(position);
+
+      TextView line1 = (TextView) view.findViewById(R.id.line_1);
+      if (line1 != null) {
+        line1.setText(prediction.getAutocompletePrediction().getPrimaryText());
+      }
+
+      TextView line2 = (TextView) view.findViewById(R.id.line_2);
+      line2.setText(prediction.getAutocompletePrediction().getSecondaryText());
+
+      return view;
+    }
+  }
+}
diff --git a/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java b/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java
index a674fec..c1d11a4 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressUiComponent.java
@@ -20,6 +20,7 @@
 import com.google.i18n.addressinput.common.RegionData;
 
 import android.view.View;
+import android.widget.AutoCompleteTextView;
 import android.widget.EditText;
 import android.widget.Spinner;
 
@@ -109,6 +110,37 @@
     }
   }
 
+  /**
+   * Sets the value displayed in the input field.
+   */
+  void setValue(String value) {
+    if (view == null) {
+      return;
+    }
+
+    switch(uiType) {
+      case SPINNER:
+        for (int i = 0; i < candidatesList.size(); i++) {
+          // Assumes that the indices in the candidate list are the same as those used in the
+          // Adapter backing the Spinner.
+          if (candidatesList.get(i).getKey().equals(value)) {
+            ((Spinner) view).setSelection(i);
+          }
+        }
+        return;
+      case EDIT:
+        if (view instanceof AutoCompleteTextView) {
+          // Prevent the AutoCompleteTextView from showing the dropdown.
+          ((AutoCompleteTextView) view).setText(value, false);
+        } else {
+          ((EditText) view).setText(value);
+        }
+        return;
+      default:
+        return;
+    }
+  }
+
   String getFieldName() {
     return fieldName;
   }
diff --git a/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java b/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java
index 02ee02b..e534327 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressWidget.java
@@ -16,6 +16,23 @@
 
 package com.android.i18n.addressinput;
 
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.Handler;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.Spinner;
+import android.widget.TextView;
+import com.android.i18n.addressinput.AddressUiComponent.UiComponent;
+import com.google.i18n.addressinput.common.AddressAutocompleteApi;
 import com.google.i18n.addressinput.common.AddressData;
 import com.google.i18n.addressinput.common.AddressDataKey;
 import com.google.i18n.addressinput.common.AddressField;
@@ -34,27 +51,10 @@
 import com.google.i18n.addressinput.common.LookupKey;
 import com.google.i18n.addressinput.common.LookupKey.KeyType;
 import com.google.i18n.addressinput.common.LookupKey.ScriptType;
+import com.google.i18n.addressinput.common.OnAddressSelectedListener;
 import com.google.i18n.addressinput.common.RegionData;
 import com.google.i18n.addressinput.common.StandardAddressVerifier;
 import com.google.i18n.addressinput.common.Util;
-
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Handler;
-import android.telephony.TelephonyManager;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.LinearLayout.LayoutParams;
-import android.widget.Spinner;
-import android.widget.TextView;
-
-import com.android.i18n.addressinput.AddressUiComponent.UiComponent;
-
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -95,6 +95,10 @@
 
   private String currentRegion;
 
+  private boolean autocompleteEnabled = false;
+
+  private AddressAutocompleteController autocompleteController;
+
   // The current language the widget uses in BCP47 format. It differs from the default locale of
   // the phone in that it contains information on the script to use.
   private String widgetLocale;
@@ -254,10 +258,33 @@
       rootView.addView(textView, lp);
     }
     if (field.getUiType().equals(UiComponent.EDIT)) {
-      EditText editText = componentProvider.createUiTextField(widthType);
-      field.setView(editText);
-      editText.setEnabled(!readOnly);
-      rootView.addView(editText, lp);
+      if (autocompleteEnabled && field.getId() == AddressField.ADDRESS_LINE_1) {
+        AutoCompleteTextView autocomplete =
+            componentProvider.createUiAutoCompleteTextField(widthType);
+        autocomplete.setEnabled(!readOnly);
+        autocompleteController.setView(autocomplete);
+        autocompleteController.setOnAddressSelectedListener(
+            new OnAddressSelectedListener() {
+              @Override
+              public void onAddressSelected(AddressData addressData) {
+                // Autocompletion will never return the recipient or the organization, so we don't
+                // want to overwrite those fields. We copy the recipient and organization fields
+                // over to avoid this.
+                AddressData current = AddressWidget.this.getAddressData();
+                AddressWidget.this.renderFormWithSavedAddress(AddressData.builder(addressData)
+                    .setRecipient(current.getRecipient())
+                    .setOrganization(current.getOrganization())
+                    .build());
+              }
+            });
+        field.setView(autocomplete);
+        rootView.addView(autocomplete, lp);
+      } else {
+        EditText editText = componentProvider.createUiTextField(widthType);
+        field.setView(editText);
+        editText.setEnabled(!readOnly);
+        rootView.addView(editText, lp);
+      }
     } else if (field.getUiType().equals(UiComponent.SPINNER)) {
       ArrayAdapter<String> adapter = componentProvider.createUiPickerAdapter(widthType);
       Spinner spinner = componentProvider.createUiPickerSpinner(widthType);
@@ -278,6 +305,20 @@
     }
   }
 
+  private void createViewForCountry() {
+    if (!formOptions.isHidden(AddressField.COUNTRY)) {
+      // For initialization when the form is first created.
+      if (!inputWidgets.containsKey(AddressField.COUNTRY)) {
+        buildCountryListBox();
+      }
+      createView(
+          rootView,
+          inputWidgets.get(AddressField.COUNTRY),
+          getLocalCountryName(currentRegion),
+          formOptions.isReadonly(AddressField.COUNTRY));
+    }
+  }
+
   /**
    *  Associates each field with its corresponding AddressUiComponent.
    */
@@ -428,6 +469,7 @@
 
   private void updateFields() {
     removePreviousViews();
+    createViewForCountry();
     buildFieldWidgets();
     initializeDropDowns();
     layoutAddressFields();
@@ -437,15 +479,7 @@
     if (rootView == null) {
       return;
     }
-    int childCount = rootView.getChildCount();
-    if (formOptions.isHidden(AddressField.COUNTRY)) {
-      if (childCount > 0) {
-        rootView.removeAllViews();
-      }
-    } else if (childCount > 2) {
-      // Keep the TextView and Spinner for Country and remove everything else.
-      rootView.removeViews(2, rootView.getChildCount() - 2);
-    }
+    rootView.removeAllViews();
   }
 
   private void layoutAddressFields() {
@@ -510,6 +544,7 @@
   }
 
   public void renderForm() {
+    createViewForCountry();
     setWidgetLocaleAndScript();
     AddressData data = new AddressData.Builder().setCountry(currentRegion)
         .setLanguageCode(widgetLocale).build();
@@ -620,10 +655,46 @@
     renderFormWithSavedAddress(savedAddress);
   }
 
+  /*
+   * Enables autocompletion for the ADDRESS_LINE_1 field. With autocompletion enabled, the user
+   * will see suggested addresses in a dropdown menu below the ADDRESS_LINE_1 field as they are
+   * typing, and when they select an address, the form fields will be autopopulated with the
+   * selected address.
+   *
+   * NOTE: This feature is currently experimental.
+   *
+   * If the AddressAutocompleteApi is not configured correctly, then the AddressWidget will degrade
+   * gracefully to an ordinary plain text input field without autocomplete.
+   */
+  public void enableAutocomplete(
+      AddressAutocompleteApi autocompleteApi, PlaceDetailsApi placeDetailsApi) {
+    AddressAutocompleteController autocompleteController =
+        new AddressAutocompleteController(context, autocompleteApi, placeDetailsApi);
+    if (autocompleteApi.isConfiguredCorrectly()) {
+      this.autocompleteEnabled = true;
+      this.autocompleteController = autocompleteController;
+
+      // The autocompleteEnabled variable set above is used in createView to determine whether to
+      // use an EditText or an AutoCompleteTextView. Re-rendering the form here ensures that
+      // createView is called with the updated value of autocompleteEnabled.
+      renderFormWithSavedAddress(getAddressData());
+    } else {
+      Log.w(
+          this.toString(),
+          "Autocomplete not configured correctly, falling back to a plain text " + "input field.");
+    }
+  }
+
+  public void disableAutocomplete() {
+    this.autocompleteEnabled = false;
+  }
+
   public void renderFormWithSavedAddress(AddressData savedAddress) {
     setWidgetLocaleAndScript();
     removePreviousViews();
+    createViewForCountry();
     buildFieldWidgets();
+    initializeDropDowns();
     layoutAddressFields();
     initializeFieldsWithAddress(savedAddress);
   }
@@ -634,10 +705,10 @@
       if (value == null) {
         value = "";
       }
+
       AddressUiComponent uiComponent = inputWidgets.get(field);
-      EditText view = (EditText) uiComponent.getView();
-      if (view != null) {
-        view.setText(value);
+      if (uiComponent != null) {
+        uiComponent.setValue(value);
       }
     }
   }
@@ -648,17 +719,11 @@
     this.rootView = rootView;
     this.formOptions = formOptions;
     // Inject Adnroid specific async request implementation here.
-    this.cacheData = new CacheData(cacheManager, new AndroidAsyncRequestApi());
+    this.cacheData = new CacheData(cacheManager, new AndroidAsyncEncodedRequestApi());
     this.clientData = new ClientData(cacheData);
     this.formController = new FormController(clientData, widgetLocale, currentRegion);
     this.formatInterpreter = new FormatInterpreter(formOptions);
     this.verifier = new StandardAddressVerifier(new FieldVerifier(clientData));
-    if (!formOptions.isHidden(AddressField.COUNTRY)) {
-      buildCountryListBox();
-      createView(rootView, inputWidgets.get(AddressField.COUNTRY),
-          getLocalCountryName(currentRegion),
-          formOptions.isReadonly(AddressField.COUNTRY));
-    }
   }
 
   /**
diff --git a/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java b/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java
index def5d72..6931847 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AddressWidgetUiComponentProvider.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
 import android.widget.EditText;
 import android.widget.Spinner;
 import android.widget.TextView;
@@ -76,6 +77,17 @@
   }
 
   /**
+   * Creates an {@link AutoCompleteTextView} for an input field that uses autocomplete.
+   *
+   * @param widthType {@link WidthType} of the field
+   * @return a custom {@link AutoCompleteTextView} created for the field
+   */
+  protected AutoCompleteTextView createUiAutoCompleteTextField(WidthType widthType) {
+    return (AutoCompleteTextView)
+        inflater.inflate(R.layout.address_autocomplete_textview, null, false);
+  }
+
+  /**
    * Creates an {@link ArrayAdapter} to work with the custom {@link Spinner} of a input field that
    * uses UI picker.
    *
diff --git a/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java
new file mode 100644
index 0000000..bc7305c
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncEncodedRequestApi.java
@@ -0,0 +1,46 @@
+package com.android.i18n.addressinput;
+
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+
+public class AndroidAsyncEncodedRequestApi extends AndroidAsyncRequestApi {
+  /**
+   * A quick hack to transform a string into an RFC 3986 compliant URL.
+   *
+   * <p>TODO: Refactor the code to stop passing URLs around as strings, to eliminate the need for
+   * this broken hack.
+   */
+  @Override
+  protected URL stringToUrl(String url) throws MalformedURLException {
+    int length = url.length();
+    StringBuilder tmp = new StringBuilder(length);
+
+    try {
+      for (int i = 0; i < length; i++) {
+        int j = i;
+        char c = '\0';
+        for (; j < length; j++) {
+          c = url.charAt(j);
+          if (c == ':' || c == '/') {
+            break;
+          }
+        }
+        if (j == length) {
+          tmp.append(URLEncoder.encode(url.substring(i), "UTF-8"));
+          break;
+        } else if (j > i) {
+          tmp.append(URLEncoder.encode(url.substring(i, j), "UTF-8"));
+          tmp.append(c);
+          i = j;
+        } else {
+          tmp.append(c);
+        }
+      }
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e); // Impossible.
+    }
+    return new URL(tmp.toString());
+  }
+}
diff --git a/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java
index c507ca5..5fe40e9 100644
--- a/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java
+++ b/android/src/main/java/com/android/i18n/addressinput/AndroidAsyncRequestApi.java
@@ -20,11 +20,9 @@
 import com.google.i18n.addressinput.common.JsoMap;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.net.URLEncoder;
 import java.security.Provider;
 import java.security.Security;
 import javax.net.ssl.HttpsURLConnection;
@@ -41,14 +39,16 @@
  */
 // TODO: Reimplement this class according to current best-practice for asynchronous requests.
 public class AndroidAsyncRequestApi implements AsyncRequestApi {
+  private static final String TAG = "AsyncRequestApi";
+
   /** Simple implementation of asynchronous HTTP GET. */
   private static class AsyncHttp extends Thread {
-    private final String requestUrlString;
+    private final URL requestUrl;
     private final AsyncCallback callback;
     private final int timeoutMillis;
 
-    protected AsyncHttp(String requestUrlString, AsyncCallback callback, int timeoutMillis) {
-      this.requestUrlString = requestUrlString;
+    protected AsyncHttp(URL requestUrl, AsyncCallback callback, int timeoutMillis) {
+      this.requestUrl = requestUrl;
       this.callback = callback;
       this.timeoutMillis = timeoutMillis;
     }
@@ -60,8 +60,7 @@
         // issues with the HTTP request, we're handling them the same way because the URLs are often
         // generated based on data returned by previous HTTP requests and we need robust, graceful
         // handling of any issues.
-        URL url = encodeUrl(requestUrlString);
-        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection();
         connection.setConnectTimeout(timeoutMillis);
         connection.setReadTimeout(timeoutMillis);
 
@@ -99,43 +98,14 @@
   }
 
   @Override public void requestObject(String url, AsyncCallback callback, int timeoutMillis) {
-    (new AsyncHttp(url, callback, timeoutMillis)).start();
+    try {
+      (new AsyncHttp(stringToUrl(url), callback, timeoutMillis)).start();
+    } catch (MalformedURLException e) {
+      callback.onFailure();
+    }
   }
 
-  /**
-   * A quick hack to transform a string into an RFC 3986 compliant URL.
-   *
-   * TODO: Refactor the code to stop passing URLs around as strings, to eliminate the need for
-   * this broken hack.
-   */
-  private static URL encodeUrl(String url) throws MalformedURLException {
-    int length = url.length();
-    StringBuilder tmp = new StringBuilder(length);
-
-    try {
-      for (int i = 0; i < length; i++) {
-        int j = i;
-        char c = '\0';
-        for (; j < length; j++) {
-          c = url.charAt(j);
-          if (c == ':' || c == '/') {
-            break;
-          }
-        }
-        if (j == length) {
-          tmp.append(URLEncoder.encode(url.substring(i), "UTF-8"));
-          break;
-        } else if (j > i) {
-          tmp.append(URLEncoder.encode(url.substring(i, j), "UTF-8"));
-          tmp.append(c);
-          i = j;
-        } else {
-          tmp.append(c);
-        }
-      }
-    } catch (UnsupportedEncodingException e) {
-      throw new AssertionError(e);  // Impossible.
-    }
-    return new URL(tmp.toString());
+  protected URL stringToUrl(String url) throws MalformedURLException {
+    return new URL(url);
   }
 }
diff --git a/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java
new file mode 100644
index 0000000..3bbb617
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsApi.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.i18n.addressinput;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import com.google.i18n.addressinput.common.AddressData;
+
+/**
+ * An interface for transforming an {@link AddressAutocompletePrediction} into {@link AddressData}.
+ */
+public interface PlaceDetailsApi {
+  ListenableFuture<AddressData> getAddressData(AddressAutocompletePrediction prediction);
+}
diff --git a/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java
new file mode 100644
index 0000000..e778911
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/PlaceDetailsClient.java
@@ -0,0 +1,151 @@
+package com.android.i18n.addressinput;
+
+import android.net.Uri;
+import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import com.google.i18n.addressinput.common.AddressData;
+import com.google.i18n.addressinput.common.AsyncRequestApi;
+import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback;
+import com.google.i18n.addressinput.common.JsoMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Implementation of the PlaceDetailsApi using the Place Details Web API
+ * (https://developers.google.com/places/web-service/details). Unfortunately, the Google Place
+ * Details API for Android does not include a structured representation of the address.
+ */
+class PlaceDetailsClient implements PlaceDetailsApi {
+
+  private AsyncRequestApi asyncRequestApi;
+  private String apiKey;
+
+  @VisibleForTesting static final int TIMEOUT = 5000;
+
+  private static final String TAG = "PlaceDetailsClient";
+
+  PlaceDetailsClient(String apiKey, AsyncRequestApi asyncRequestApi) {
+    this.asyncRequestApi = asyncRequestApi;
+    this.apiKey = apiKey;
+  }
+
+  @Override
+  public ListenableFuture<AddressData> getAddressData(AddressAutocompletePrediction prediction) {
+    final SettableFuture<AddressData> addressData = SettableFuture.create();
+
+    asyncRequestApi.requestObject(
+        new Uri.Builder()
+            .scheme("https")
+            .authority("maps.googleapis.com")
+            .path("maps/api/place/details/json")
+            .appendQueryParameter("key", apiKey)
+            .appendQueryParameter("placeid", prediction.getPlaceId())
+            .build()
+            .toString(),
+        new AsyncCallback() {
+          @Override
+          public void onFailure() {
+            addressData.cancel(false);
+          }
+
+          @Override
+          public void onSuccess(JsoMap response) {
+            // Can't use JSONObject#getJSONObject to get the 'result' because #getJSONObject calls
+            // #get, which has been broken by JsoMap to only return String values
+            // *grinds teeth in frustration*.
+            try {
+              Object result = response.getObject("result");
+              if (result instanceof JSONObject) {
+                addressData.set(getAddressData((JSONObject) result));
+              } else {
+                Log.e(
+                    TAG,
+                    "Error parsing JSON response from Place Details API: "
+                        + "expected 'result' field.");
+                onFailure();
+              }
+            } catch (JSONException e) {
+              Log.e(TAG, "Error parsing JSON response from Place Details API", e);
+              onFailure();
+            }
+          }
+        },
+        TIMEOUT);
+
+    return addressData;
+  }
+
+  private AddressData getAddressData(JSONObject result) throws JSONException {
+    AddressData.Builder addressData = AddressData.builder();
+
+    // Get the country code from address_components.
+    JSONArray addressComponents = result.getJSONArray("address_components");
+    // Iterate backwards since country is usually at the end.
+    for (int i = addressComponents.length() - 1; i >= 0; i--) {
+      JSONObject addressComponent = addressComponents.getJSONObject(i);
+
+      List<String> types = new ArrayList<>();
+      JSONArray componentTypes = addressComponent.getJSONArray("types");
+      for (int j = 0; j < componentTypes.length(); j++) {
+        types.add(componentTypes.getString(j));
+      }
+
+      if (types.contains("country")) {
+        addressData.setCountry(addressComponent.getString("short_name"));
+        break;
+      }
+    }
+
+    String unescapedAdrAddress =
+        result
+            .getString("adr_address")
+            .replace("\\\"", "\"")
+            .replace("\\u003c", "<")
+            .replace("\\u003e", ">");
+
+    Pattern adrComponentPattern = Pattern.compile("[, ]{0,2}<span class=\"(.*)\">(.*)<");
+
+    for (String adrComponent : unescapedAdrAddress.split("/span>")) {
+      Matcher m = adrComponentPattern.matcher(adrComponent);
+      Log.i(TAG, adrComponent + " matches: " + m.matches());
+      if (m.matches() && m.groupCount() == 2) {
+        String key = m.group(1);
+        String value = m.group(2);
+        switch (key) {
+          case "street-address":
+            addressData.setAddress(value);
+            // TODO(b/33790911): Include the 'extended-address' and 'post-office-box' adr_address
+            // fields in the AddressData address.
+            break;
+          case "locality":
+            addressData.setLocality(value);
+            break;
+          case "region":
+            addressData.setAdminArea(value);
+            break;
+          case "postal-code":
+            addressData.setPostalCode(value);
+            break;
+          case "country-name":
+            // adr_address country names are not in CLDR format, which is the format used by the
+            // AddressWidget. We fetch the country code from the address_components instead.
+            break;
+          default:
+            Log.e(TAG, "Key " + key + " not recognized in Place Details API response.");
+        }
+      } else {
+        Log.e(TAG, "Failed to match " + adrComponent + " with regexp " + m.pattern().toString());
+      }
+    }
+
+    return addressData.build();
+  }
+}
diff --git a/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java
new file mode 100644
index 0000000..1b8d9a0
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompleteApiImpl.java
@@ -0,0 +1,105 @@
+package com.android.i18n.addressinput.autocomplete.gmscore;
+
+import android.location.Location;
+import android.util.Log;
+import com.google.android.gms.common.api.GoogleApiClient;
+import com.google.android.gms.common.api.ResultCallback;
+import com.google.android.gms.location.FusedLocationProviderApi;
+import com.google.android.gms.location.places.AutocompleteFilter;
+import com.google.android.gms.location.places.AutocompletePrediction;
+import com.google.android.gms.location.places.AutocompletePredictionBuffer;
+import com.google.android.gms.location.places.GeoDataApi;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.LatLngBounds;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.i18n.addressinput.common.AddressAutocompleteApi;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * GMSCore implementation of {@link com.google.i18n.addressinput.common.AddressAutocompleteApi}.
+ *
+ * Callers should provide a GoogleApiClient with the Places.GEO_DATA_API and
+ * LocationServices.API enabled. The GoogleApiClient should be connected before
+ * it is passed to AddressWidget#enableAutocomplete. The caller will also need to request the
+ * following permissions in their AndroidManifest.xml:
+ *
+ * <pre>
+ *   <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
+ *   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ * </pre>
+ *
+ * Callers should check that the required permissions are actually present.
+ * TODO(b/32559817): Handle permission check in libaddressinput so that callers don't need to.
+ */
+public class AddressAutocompleteApiImpl implements AddressAutocompleteApi {
+
+  private static final String TAG = "GmsCoreAddrAutocmplt";
+  private GoogleApiClient googleApiClient;
+
+  // Use Places.GeoDataApi.
+  private GeoDataApi geoDataApi;
+
+  // Use LocationServices.FusedLocationApi.
+  private FusedLocationProviderApi locationApi;
+
+  public AddressAutocompleteApiImpl(
+      GoogleApiClient googleApiClient,
+      GeoDataApi geoDataApi,
+      FusedLocationProviderApi locationApi) {
+    this.googleApiClient = googleApiClient;
+    this.geoDataApi = geoDataApi;
+    this.locationApi = locationApi;
+  }
+
+  // TODO(b/32559817): Add a check to ensure that the required permissions have been granted.
+  @Override
+  public boolean isConfiguredCorrectly() {
+    if (!googleApiClient.isConnected()) {
+      Log.e(TAG, "Cannot get autocomplete predictions because Google API client is not connected.");
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public void getAutocompletePredictions(
+      String query, final FutureCallback<List<? extends AddressAutocompletePrediction>> callback) {
+    Location deviceLocation = locationApi.getLastLocation(googleApiClient);
+    LatLngBounds bounds =
+        deviceLocation == null
+            ? null
+            : LatLngBounds.builder()
+                .include(new LatLng(deviceLocation.getLatitude(), deviceLocation.getLongitude()))
+                .build();
+
+    geoDataApi
+        .getAutocompletePredictions(
+            googleApiClient,
+            query,
+            bounds,
+            new AutocompleteFilter.Builder()
+                .setTypeFilter(AutocompleteFilter.TYPE_FILTER_ADDRESS)
+                .build())
+        .setResultCallback(
+            new ResultCallback<AutocompletePredictionBuffer>() {
+              @Override
+              public void onResult(AutocompletePredictionBuffer resultBuffer) {
+                callback.onSuccess(convertPredictions(resultBuffer));
+              }
+            });
+  }
+
+  private List<? extends AddressAutocompletePrediction> convertPredictions(
+      AutocompletePredictionBuffer resultBuffer) {
+    List<AddressAutocompletePrediction> predictions = new ArrayList<>();
+
+    for (AutocompletePrediction prediction : resultBuffer) {
+      predictions.add(new AddressAutocompletePredictionImpl(prediction));
+    }
+
+    return predictions;
+  }
+}
diff --git a/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java
new file mode 100644
index 0000000..d38b450
--- /dev/null
+++ b/android/src/main/java/com/android/i18n/addressinput/autocomplete/gmscore/AddressAutocompletePredictionImpl.java
@@ -0,0 +1,32 @@
+package com.android.i18n.addressinput.autocomplete.gmscore;
+
+import com.google.android.gms.location.places.AutocompletePrediction;
+import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
+
+/**
+ * GMSCore implementation of {@link
+ * com.google.i18n.addressinput.common.AddressAutocompletePrediction}.
+ */
+public class AddressAutocompletePredictionImpl extends AddressAutocompletePrediction {
+
+  private AutocompletePrediction prediction;
+
+  AddressAutocompletePredictionImpl(AutocompletePrediction prediction) {
+    this.prediction = prediction;
+  }
+
+  @Override
+  public String getPlaceId() {
+    return prediction.getPlaceId();
+  }
+
+  @Override
+  public CharSequence getPrimaryText() {
+    return prediction.getPrimaryText(null);
+  }
+
+  @Override
+  public CharSequence getSecondaryText() {
+    return prediction.getSecondaryText(null);
+  }
+}
diff --git a/android/src/main/res/drawable-v19/autocomplete_dropdown_item_background_selected.xml b/android/src/main/res/drawable-v19/autocomplete_dropdown_item_background_selected.xml
new file mode 100644
index 0000000..cf77743
--- /dev/null
+++ b/android/src/main/res/drawable-v19/autocomplete_dropdown_item_background_selected.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+The <ripple> element is only available after API level 21, so this polyfill causes the background
+of autocomplete dropdown items to darken when they are clicked.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true">
+        <shape>
+            <solid android:drawable="@color/ripple_material_light"/>
+        </shape>
+    </item>
+
+    <item>
+        <shape>
+            <solid android:drawable="@android:color/white"/>
+        </shape>
+    </item>
+</selector>
diff --git a/android/src/main/res/drawable-v21/autocomplete_dropdown_item_background_selected.xml b/android/src/main/res/drawable-v21/autocomplete_dropdown_item_background_selected.xml
new file mode 100644
index 0000000..60b404e
--- /dev/null
+++ b/android/src/main/res/drawable-v21/autocomplete_dropdown_item_background_selected.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/ripple_material_light">
+
+    <item android:drawable="@android:color/white" />
+</ripple>
diff --git a/android/src/main/res/layout/address_autocomplete_dropdown_item.xml b/android/src/main/res/layout/address_autocomplete_dropdown_item.xml
new file mode 100644
index 0000000..57841a8
--- /dev/null
+++ b/android/src/main/res/layout/address_autocomplete_dropdown_item.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingTop="@dimen/address_autocomplete_dropdown_item_padding_vertical"
+    android:paddingBottom="@dimen/address_autocomplete_dropdown_item_padding_vertical"
+    android:background="@drawable/autocomplete_dropdown_item_background_selected">
+
+  <TextView
+      style="?android:attr/dropDownItemStyle"
+      android:id="@+id/line_1"
+      android:textAppearance="?android:attr/textAppearanceSmallPopupMenu"
+      android:singleLine="true"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:gravity="left"
+      android:background="@android:color/transparent"/>
+
+  <TextView
+      style="?android:attr/dropDownItemStyle"
+      android:id="@+id/line_2"
+      android:textAppearance="?android:attr/textAppearanceSmallPopupMenu"
+      android:singleLine="true"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:gravity="left"
+      android:background="@android:color/transparent"/>
+
+</LinearLayout>
diff --git a/android/src/main/res/layout/address_autocomplete_textview.xml b/android/src/main/res/layout/address_autocomplete_textview.xml
new file mode 100644
index 0000000..436626b
--- /dev/null
+++ b/android/src/main/res/layout/address_autocomplete_textview.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/**
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<AutoCompleteTextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/address_autocomplete_text_view"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="@dimen/address_textview_margin_top"
+    android:layout_marginLeft="@dimen/address_textview_margin_left"
+    android:textColor="?android:attr/textColorPrimary"
+    android:focusableInTouchMode="true" />
diff --git a/android/src/main/res/layout/address_textview.xml b/android/src/main/res/layout/address_textview.xml
index f112a5a..1fe05e5 100644
--- a/android/src/main/res/layout/address_textview.xml
+++ b/android/src/main/res/layout/address_textview.xml
@@ -20,7 +20,7 @@
     android:id="@+id/address_text_view"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
-    android:layout_marginTop="4dip"
-    android:layout_marginLeft="3dip"
+    android:layout_marginTop="@dimen/address_textview_margin_top"
+    android:layout_marginLeft="@dimen/address_textview_margin_left"
     android:textColor="?android:attr/textColorPrimary"
     android:focusableInTouchMode="true" />
diff --git a/android/src/main/res/values/address_dimens.xml b/android/src/main/res/values/address_dimens.xml
new file mode 100644
index 0000000..6ba89b6
--- /dev/null
+++ b/android/src/main/res/values/address_dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<resources>
+  <dimen name="address_textview_margin_top">4dp</dimen>
+  <dimen name="address_textview_margin_left">3dp</dimen>
+
+  <dimen name="address_autocomplete_dropdown_item_padding_vertical">5dp</dimen>
+</resources>
diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d25b5cd
--- /dev/null
+++ b/android/src/main/res/values/colors.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+  <item name="ripple_material_light" type="color">#1f000000</item>
+
+  <integer-array name="androidcolors">
+      <item>@color/ripple_material_light</item>
+  </integer-array>
+</resources>
diff --git a/common/README b/common/README.md
similarity index 73%
rename from common/README
rename to common/README.md
index fdec1bd..1e58ecd 100644
--- a/common/README
+++ b/common/README.md
@@ -1,5 +1,5 @@
-Building and running tests
-==========================
+# Building and running tests
+
 
 The common (non-UI) parts of libaddressinput are built and run using the Gradle
 project automation tool:
@@ -8,8 +8,8 @@
 http://www.gradle.org/
 
 
-Prerequisite dependencies for using Gradle
-------------------------------------------
+## Prerequisite dependencies for using Gradle
+
 Gradle (latest version):
   https://services.gradle.org/distributions/gradle-2.3-bin.zip
 
@@ -17,8 +17,8 @@
 Gradle on your path, as this can cause problems.
 
 
-Building and Running
---------------------
+## Building and Running
+
 After installing all the prerequisites, check that everything is working by
 running:
 
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java
new file mode 100644
index 0000000..1dd4711
--- /dev/null
+++ b/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompleteApi.java
@@ -0,0 +1,27 @@
+package com.google.i18n.addressinput.common;
+
+import com.google.common.util.concurrent.FutureCallback;
+import java.util.List;
+
+/**
+ * AddressAutocompleteApi encapsulates the functionality required to fetch address autocomplete
+ * suggestions for an unstructured address query string entered by the user.
+ *
+ * An implementation using GMSCore is provided under
+ * libaddressinput/android/src/main.java/com/android/i18n/addressinput/autocomplete/gmscore.
+ */
+public interface AddressAutocompleteApi {
+  /**
+   * Returns true if the AddressAutocompleteApi is properly configured to fetch autocomplete
+   * predictions. This allows the caller to enable autocomplete only if the AddressAutocompleteApi
+   * is properly configured (e.g. the user has granted all the necessary permissions).
+   */
+  boolean isConfiguredCorrectly();
+
+  /**
+   * Given an unstructured address query, getAutocompletePredictions fetches autocomplete
+   * suggestions for the intended address and provides these suggestions via the callback.
+   */
+  void getAutocompletePredictions(
+      String query, FutureCallback<List<? extends AddressAutocompletePrediction>> callback);
+}
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java
new file mode 100644
index 0000000..6f788a9
--- /dev/null
+++ b/common/src/main/java/com/google/i18n/addressinput/common/AddressAutocompletePrediction.java
@@ -0,0 +1,51 @@
+package com.google.i18n.addressinput.common;
+
+import java.util.Objects;
+
+/**
+ * AddressAutocompletePrediction represents an autocomplete suggestion.
+ *
+ * Concrete inheriting classes must provide implementations of {@link #getPlaceId}, {@link
+ * #getPrimaryText}, and {@link #getSecondaryText}. An implementation using GMSCore is provided
+ * under libaddressinput/android/src/main.java/com/android/i18n/addressinput/autocomplete/gmscore.
+ */
+public abstract class AddressAutocompletePrediction {
+  /**
+   * Returns the place ID of the predicted place. A place ID is a textual identifier that uniquely
+   * identifies a place, which you can use to retrieve the Place object again later (for example,
+   * with Google's Place Details Web API).
+   */
+  public abstract String getPlaceId();
+
+  /**
+   * Returns the main text describing a place. This is usually the name of the place. Examples:
+   * "Eiffel Tower", and "123 Pitt Street".
+   */
+  public abstract CharSequence getPrimaryText();
+
+  /**
+   * Returns the subsidiary text of a place description. This is useful, for example, as a second
+   * line when showing autocomplete predictions. Examples: "Avenue Anatole France, Paris, France",
+   * and "Sydney, New South Wales".
+   */
+  public abstract CharSequence getSecondaryText();
+
+  // equals and hashCode overridden for testing.
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof AddressAutocompletePrediction)) {
+      return false;
+    }
+    AddressAutocompletePrediction p = (AddressAutocompletePrediction) o;
+
+    return getPlaceId().equals(p.getPlaceId())
+        && getPrimaryText().equals(p.getPrimaryText())
+        && getSecondaryText().equals(p.getSecondaryText());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getPlaceId(), getPrimaryText(), getSecondaryText());
+  }
+}
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java
index 3e2c10e..e30a89a 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/AddressData.java
@@ -136,6 +136,9 @@
   // BCP-47 language code for the address. Can be set to null.
   private final String languageCode;
 
+  // NOTE: If you add a new field which is semantically significant, you must also add a check for
+  // that field in {@link equals} and {@link hashCode}.
+
   private AddressData(Builder builder) {
     this.postalCountry = builder.fields.get(AddressField.COUNTRY);
     this.administrativeArea = builder.fields.get(AddressField.ADMIN_AREA);
@@ -205,6 +208,76 @@
     return output.toString();
   }
 
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof AddressData)) {
+      return false;
+    }
+    AddressData addressData = (AddressData) o;
+
+    return (postalCountry == null
+            ? addressData.getPostalCountry() == null
+            : postalCountry.equals(addressData.getPostalCountry()))
+        && (addressLines == null
+            ? addressData.getAddressLines() == null
+            : addressLines.equals(addressData.getAddressLines()))
+        && (administrativeArea == null
+            ? addressData.getAdministrativeArea() == null
+            : this.getAdministrativeArea().equals(addressData.getAdministrativeArea()))
+        && (locality == null
+            ? addressData.getLocality() == null
+            : locality.equals(addressData.getLocality()))
+        && (dependentLocality == null
+            ? addressData.getDependentLocality() == null
+            : dependentLocality.equals(addressData.getDependentLocality()))
+        && (postalCode == null
+            ? addressData.getPostalCode() == null
+            : postalCode.equals(addressData.getPostalCode()))
+        && (sortingCode == null
+            ? addressData.getSortingCode() == null
+            : sortingCode.equals(addressData.getSortingCode()))
+        && (organization == null
+            ? addressData.getOrganization() == null
+            : organization.equals(addressData.getOrganization()))
+        && (recipient == null
+            ? addressData.getRecipient() == null
+            : recipient.equals(addressData.getRecipient()))
+        && (languageCode == null
+            ? this.getLanguageCode() == null
+            : languageCode.equals(addressData.getLanguageCode()));
+  }
+
+  @Override
+  public int hashCode() {
+    // 17 and 31 are arbitrary seed values.
+    int result = 17;
+
+    String[] fields =
+        new String[] {
+          postalCountry,
+          administrativeArea,
+          locality,
+          dependentLocality,
+          postalCode,
+          sortingCode,
+          organization,
+          recipient,
+          languageCode
+        };
+
+    for (String field : fields) {
+      result = 31 * result + (field == null ? 0 : field.hashCode());
+    }
+
+    // The only significant field which is not a String.
+    result = 31 * result + (addressLines == null ? 0 : addressLines.hashCode());
+
+    return result;
+  }
+
   /**
    * Returns the CLDR region code for this address; note that this is <em>not</em> the same as the
    * ISO 3166-1 2-letter country code. While technically optional, this field will always be set
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java b/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java
index fe31caa..906d56a 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/AddressProblems.java
@@ -18,6 +18,7 @@
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * This structure keeps track of any errors found when validating the AddressData.
@@ -65,4 +66,13 @@
   public Map<AddressField, AddressProblemType> getProblems() {
     return problems;
   }
+
+  /**
+   * Adds all problems this object contains to the given {@link AddressProblems} object.
+   */
+  public void copyInto(AddressProblems other) {
+    for (Entry<AddressField, AddressProblemType> problem : problems.entrySet()) {
+      other.add(problem.getKey(), problem.getValue());
+    }
+  }
 }
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java b/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java
index a281797..a8d9763 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/FieldVerifier.java
@@ -18,7 +18,6 @@
 
 import com.google.i18n.addressinput.common.LookupKey.KeyType;
 import com.google.i18n.addressinput.common.LookupKey.ScriptType;
-
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
@@ -47,6 +46,7 @@
   // Package-private so it can be accessed by tests.
   String id;
   private DataSource dataSource;
+  private boolean useRegionDataConstants;
 
   // Package-private so they can be accessed by tests.
   Set<AddressField> possiblyUsedFields;
@@ -71,10 +71,19 @@
   private Pattern match;
 
   /**
-   * Creates the root field verifier for a particular data source.
+   * Creates the root field verifier for a particular data source. Defaults useRegionDataConstants
+   * to true.
    */
   public FieldVerifier(DataSource dataSource) {
+    this(dataSource, true /* useRegionDataConstants */);
+  }
+
+  /**
+   * Creates the root field verifier for a particular data source.
+   */
+  public FieldVerifier(DataSource dataSource, boolean useRegionDataConstants) {
     this.dataSource = dataSource;
+    this.useRegionDataConstants = useRegionDataConstants;
     populateRootVerifier();
   }
 
@@ -85,9 +94,10 @@
    */
   FieldVerifier(FieldVerifier parent, AddressVerificationNodeData nodeData) {
     // Most information is inherited from the parent.
-    possiblyUsedFields = parent.possiblyUsedFields;
-    required = parent.required;
+    possiblyUsedFields = new HashSet<AddressField>(parent.possiblyUsedFields);
+    required = new HashSet<AddressField>(parent.required);
     dataSource = parent.dataSource;
+    useRegionDataConstants = parent.useRegionDataConstants;
     format = parent.format;
     match = parent.match;
     // Here we add in any overrides from this particular node as well as information such as
@@ -158,8 +168,6 @@
         && keys.length == latinNames.length) {
       localNames = keys;
     }
-    // These fields are populated from RegionDataConstants so that the metadata server can be
-    // updated without needing to be in sync with clients.
     if (isCountryKey()) {
       populatePossibleAndRequired(getRegionCodeFromKey(id));
     }
@@ -183,11 +191,7 @@
   private Set<String> getAcceptableAlternateLanguages(String regionCode) {
     // TODO: We should have a class that knows how to get information about the data, rather than
     // getting the node and extracting keys here.
-    LookupKey lookupKey =
-        new LookupKey.Builder(Util.toLowerCaseLocaleIndependent(KeyType.DATA.name())
-            + KEY_NODE_DELIMITER
-            + regionCode).build();
-    AddressVerificationNodeData countryNode = dataSource.getDefaultData(lookupKey.toString());
+    AddressVerificationNodeData countryNode = getCountryNode(regionCode);
     String languages = countryNode.get(AddressDataKey.LANGUAGES);
     String defaultLanguage = countryNode.get(AddressDataKey.LANG);
     Set<String> alternateLanguages = new HashSet<String>();
@@ -204,7 +208,40 @@
     return alternateLanguages;
   }
 
+  private AddressVerificationNodeData getCountryNode(String regionCode) {
+    LookupKey lookupKey = new LookupKey.Builder(KeyType.DATA)
+        .setAddressData(new AddressData.Builder().setCountry(regionCode).build())
+        .build();
+    return dataSource.getDefaultData(lookupKey.toString());
+  }
+
   private void populatePossibleAndRequired(String regionCode) {
+    // If useRegionDataConstants is true, these fields are populated from RegionDataConstants so
+    // that the metadata server can be updated without needing to be in sync with clients;
+    // otherwise, these fields are populated from dataSource.
+    if (!useRegionDataConstants) {
+      AddressVerificationNodeData countryNode = getCountryNode(regionCode);
+      AddressVerificationNodeData defaultNode = getCountryNode("ZZ");
+
+      String formatString = countryNode.get(AddressDataKey.FMT);
+      if (formatString == null) {
+        formatString = defaultNode.get(AddressDataKey.FMT);
+      }
+      if (formatString != null) {
+        List<AddressField> possible =
+            FORMAT_INTERPRETER.getAddressFieldOrder(formatString, regionCode);
+        possiblyUsedFields.addAll(convertAddressFieldsToPossiblyUsedSet(possible));
+      }  /* else: shouldn't ever happen */
+      String requireString = countryNode.get(AddressDataKey.REQUIRE);
+      if (requireString == null) {
+        requireString = defaultNode.get(AddressDataKey.REQUIRE);
+      }
+      if (requireString != null) {
+        required = FormatInterpreter.getRequiredFields(requireString, regionCode);
+      }  /* else: shouldn't ever happen */
+      return;
+    }
+
     List<AddressField> possible =
         FORMAT_INTERPRETER.getAddressFieldOrder(ScriptType.LOCAL, regionCode);
     possiblyUsedFields = convertAddressFieldsToPossiblyUsedSet(possible);
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java b/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
index 1ace84f..ba4c668 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/FormatInterpreter.java
@@ -61,10 +61,15 @@
   public List<AddressField> getAddressFieldOrder(ScriptType scriptType, String regionCode) {
     Util.checkNotNull(scriptType);
     Util.checkNotNull(regionCode);
+    String formatString = getFormatString(scriptType, regionCode);
+    return getAddressFieldOrder(formatString, regionCode);
+  }
+
+  List<AddressField> getAddressFieldOrder(String formatString, String regionCode) {
     EnumSet<AddressField> visibleFields = EnumSet.noneOf(AddressField.class);
     List<AddressField> fieldOrder = new ArrayList<AddressField>();
     // TODO: Change this to just enumerate the address fields directly.
-    for (String substring : getFormatSubstrings(scriptType, regionCode)) {
+    for (String substring : getFormatSubstrings(formatString)) {
       // Skips un-escaped characters and new lines.
       if (!substring.matches("%.") || substring.equals(NEW_LINE)) {
         continue;
@@ -153,7 +158,10 @@
   static Set<AddressField> getRequiredFields(String regionCode) {
     Util.checkNotNull(regionCode);
     String requireString = getRequiredString(regionCode);
+    return getRequiredFields(requireString, regionCode);
+  }
 
+  static Set<AddressField> getRequiredFields(String requireString, String regionCode) {
     EnumSet<AddressField> required = EnumSet.of(AddressField.COUNTRY);
     for (char c : requireString.toCharArray()) {
       required.add(AddressField.of(c));
@@ -241,7 +249,8 @@
     }
 
     List<String> prunedFormat = new ArrayList<String>();
-    List<String> formatSubstrings = getFormatSubstrings(scriptType, regionCode);
+    String formatString = getFormatString(scriptType, regionCode);
+    List<String> formatSubstrings = getFormatSubstrings(formatString);
     for (int i = 0; i < formatSubstrings.size(); i++) {
       String formatSubstring = formatSubstrings.get(i);
       // Always keep the newlines.
@@ -335,8 +344,7 @@
    */
   // TODO: Create a common method which does field parsing in one place (there are about 4 other
   // places in this library where format strings are parsed).
-  private List<String> getFormatSubstrings(ScriptType scriptType, String regionCode) {
-    String formatString = getFormatString(scriptType, regionCode);
+  private List<String> getFormatSubstrings(String formatString) {
     List<String> parts = new ArrayList<String>();
 
     boolean escaped = false;
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java b/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java
index 4f039c2..cc5a2cf 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/JsoMap.java
@@ -102,7 +102,7 @@
    * @return The object associated with the key.
    * @throws JSONException if the key is not found.
    */
-  private Object getObject(String name) throws JSONException {
+  public Object getObject(String name) throws JSONException {
     return super.get(name);
   }
 
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java b/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java
new file mode 100644
index 0000000..b5b9163
--- /dev/null
+++ b/common/src/main/java/com/google/i18n/addressinput/common/OnAddressSelectedListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.i18n.addressinput.common;
+
+/**
+ * If autocomplete is enabled on the AddressWidget, setting an OnAddressSelectedListener
+ * will cause onAddressSelected to be called when the user clicks on an autocomplete
+ * suggestion in the dropdown list.
+ */
+public interface OnAddressSelectedListener {
+  void onAddressSelected(AddressData addressData);
+}
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java b/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
index 975ca76..c977695 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/RegionDataConstants.java
@@ -94,7 +94,7 @@
     map.put("EG", "{\"name\":\"EGYPT\",\"lang\":\"ar\",\"languages\":\"ar\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\"}");
     map.put("EH", "{\"name\":\"WESTERN SAHARA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("ER", "{\"name\":\"ERITREA\"}");
-    map.put("ES", "{\"name\":\"SPAIN\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"width_overrides\":\"%S:S\"}");
+    map.put("ES", "{\"name\":\"SPAIN\",\"lang\":\"es\",\"languages\":\"es~ca~gl~eu\",\"fmt\":\"%N%n%O%n%A%n%Z %C %S\",\"require\":\"ACSZ\",\"upper\":\"CS\",\"width_overrides\":\"%S:S\"}");
     map.put("ET", "{\"name\":\"ETHIOPIA\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("FI", "{\"name\":\"FINLAND\",\"fmt\":\"%O%n%N%n%A%nFI-%Z %C\",\"require\":\"ACZ\",\"postprefix\":\"FI-\"}");
     map.put("FJ", "{\"name\":\"FIJI\"}");
@@ -118,7 +118,7 @@
     map.put("GR", "{\"name\":\"GREECE\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\"}");
     map.put("GS", "{\"name\":\"SOUTH GEORGIA\",\"fmt\":\"%N%n%O%n%A%n%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("GT", "{\"name\":\"GUATEMALA\",\"fmt\":\"%N%n%O%n%A%n%Z- %C\"}");
-    map.put("GU", "{\"name\":\"GUAM\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}");
+    map.put("GU", "{\"name\":\"GUAM\",\"fmt\":\"%N%n%O%n%A%n%C %Z\",\"require\":\"ACZ\",\"upper\":\"ACNO\",\"zip_name_type\":\"zip\"}");
     map.put("GW", "{\"name\":\"GUINEA-BISSAU\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("GY", "{\"name\":\"GUYANA\"}");
     map.put("HK", "{\"name\":\"HONG KONG\",\"lang\":\"zh-Hant\",\"languages\":\"zh-Hant~en\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S\",\"fmt\":\"%S%n%C%n%A%n%O%n%N\",\"require\":\"AS\",\"upper\":\"S\",\"locality_name_type\":\"district\",\"state_name_type\":\"area\",\"width_overrides\":\"%S:S%C:L\",\"label_overrides\":[{\"field\":\"C\",\"label\":\"地区\",\"lang\":\"zh\"},{\"field\":\"C\",\"label\":\"地區\",\"lang\":\"zh-HK\"},{\"field\":\"C\",\"label\":\"地區\",\"lang\":\"zh-TW\"},{\"field\":\"CS\",\"label\":\"Flat / Room\",\"lang\":\"en\"},{\"field\":\"CS\",\"label\":\"單位編號\",\"lang\":\"zh-HK\"},{\"field\":\"BG\",\"label\":\"大廈名稱\",\"lang\":\"zh-HK\"}]}");
@@ -131,7 +131,7 @@
     map.put("IE", "{\"name\":\"IRELAND\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%D%n%C%n%S %Z\",\"sublocality_name_type\":\"townland\",\"state_name_type\":\"county\",\"zip_name_type\":\"eircode\",\"label_overrides\":[{\"field\":\"S\",\"label\":\"郡\",\"lang\":\"zh\"}]}");
     map.put("IL", "{\"name\":\"ISRAEL\",\"fmt\":\"%N%n%O%n%A%n%C %Z\"}");
     map.put("IM", "{\"name\":\"ISLE OF MAN\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
-    map.put("IN", "{\"name\":\"INDIA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"require\":\"ACSZ\",\"state_name_type\":\"state\",\"zip_name_type\":\"pin\"}");
+    map.put("IN", "{\"name\":\"INDIA\",\"lang\":\"en\",\"languages\":\"en~hi\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"require\":\"ACSZ\",\"state_name_type\":\"state\",\"zip_name_type\":\"pin\"}");
     map.put("IO", "{\"name\":\"BRITISH INDIAN OCEAN TERRITORY\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"ACZ\",\"upper\":\"CZ\"}");
     map.put("IQ", "{\"name\":\"IRAQ\",\"fmt\":\"%O%n%N%n%A%n%C, %S%n%Z\",\"require\":\"ACS\",\"upper\":\"CS\"}");
     map.put("IR", "{\"name\":\"IRAN\",\"fmt\":\"%O%n%N%n%S%n%C, %D%n%A%n%Z\",\"sublocality_name_type\":\"neighborhood\"}");
@@ -189,7 +189,7 @@
     map.put("NC", "{\"name\":\"NEW CALEDONIA\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}");
     map.put("NE", "{\"name\":\"NIGER\",\"fmt\":\"%N%n%O%n%A%n%Z %C\"}");
     map.put("NF", "{\"name\":\"NORFOLK ISLAND\",\"fmt\":\"%O%n%N%n%A%n%C %S %Z\",\"upper\":\"CS\"}");
-    map.put("NG", "{\"name\":\"NIGERIA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\",\"upper\":\"CS\",\"state_name_type\":\"state\"}");
+    map.put("NG", "{\"name\":\"NIGERIA\",\"lang\":\"en\",\"languages\":\"en\",\"fmt\":\"%N%n%O%n%A%n%D%n%C %Z%n%S\",\"upper\":\"CS\",\"state_name_type\":\"state\"}");
     map.put("NI", "{\"name\":\"NICARAGUA\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%Z%n%C, %S\",\"upper\":\"CS\",\"state_name_type\":\"department\"}");
     map.put("NL", "{\"name\":\"NETHERLANDS\",\"fmt\":\"%O%n%N%n%A%n%Z %C\",\"require\":\"ACZ\"}");
     map.put("NO", "{\"name\":\"NORWAY\",\"fmt\":\"%N%n%O%n%A%n%Z %C\",\"require\":\"ACZ\",\"locality_name_type\":\"post_town\"}");
@@ -265,7 +265,7 @@
     map.put("VE", "{\"name\":\"VENEZUELA\",\"lang\":\"es\",\"languages\":\"es\",\"fmt\":\"%N%n%O%n%A%n%C %Z, %S\",\"require\":\"ACS\",\"upper\":\"CS\",\"state_name_type\":\"state\"}");
     map.put("VG", "{\"name\":\"VIRGIN ISLANDS (BRITISH)\",\"fmt\":\"%N%n%O%n%A%n%C%n%Z\",\"require\":\"A\"}");
     map.put("VI", "{\"name\":\"VIRGIN ISLANDS (U.S.)\",\"fmt\":\"%N%n%O%n%A%n%C %S %Z\",\"require\":\"ACSZ\",\"upper\":\"ACNOS\",\"state_name_type\":\"state\",\"zip_name_type\":\"zip\"}");
-    map.put("VN", "{\"name\":\"VIET NAM\",\"lang\":\"vi\",\"languages\":\"vi\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\"}");
+    map.put("VN", "{\"name\":\"VIET NAM\",\"lang\":\"vi\",\"languages\":\"vi\",\"lfmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\",\"label_overrides\":[{\"field\":\"S1\",\"label\":\"Ward/Township/Commune\"},{\"field\":\"S1\",\"label\":\"Phường/Thị trấn/Xã\",\"lang\":\"vi\"}]}");
     map.put("VU", "{\"name\":\"VANUATU\"}");
     map.put("WF", "{\"name\":\"WALLIS AND FUTUNA ISLANDS\",\"fmt\":\"%O%n%N%n%A%n%Z %C %X\",\"require\":\"ACZ\",\"upper\":\"ACX\"}");
     map.put("WS", "{\"name\":\"SAMOA\"}");
diff --git a/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java b/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java
index e1fe22b..b9b3fda 100644
--- a/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java
+++ b/common/src/main/java/com/google/i18n/addressinput/common/StandardAddressVerifier.java
@@ -29,6 +29,7 @@
 import com.google.i18n.addressinput.common.LookupKey.ScriptType;
 
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -83,15 +84,31 @@
     verifier.start();
   }
 
+  /**
+   * Verifies only the specified fields in the address.
+   */
+  public void verifyFields(
+      AddressData address, AddressProblems problems, EnumSet<AddressField> addressFieldsToVerify) {
+    new Verifier(address, problems, new NotifyingListener(), addressFieldsToVerify).run();
+  }
+
   private class Verifier implements Runnable {
     private AddressData address;
     private AddressProblems problems;
     private DataLoadListener listener;
+    private EnumSet<AddressField> addressFieldsToVerify;
 
     Verifier(AddressData address, AddressProblems problems, DataLoadListener listener) {
+      this(address, problems, listener, EnumSet.allOf(AddressField.class));
+    }
+
+    Verifier(
+        AddressData address, AddressProblems problems, DataLoadListener listener,
+        EnumSet<AddressField> addressFieldsToVerify) {
       this.address = address;
       this.problems = problems;
       this.listener = listener;
+      this.addressFieldsToVerify = addressFieldsToVerify;
     }
 
     @Override
@@ -111,22 +128,23 @@
 
       // The first four calls refine the verifier, so must come first, and in this
       // order.
-      verifyField(script, v, COUNTRY, address.getPostalCountry(), problems);
-      if (problems.isEmpty()) {
+      verifyFieldIfSelected(script, v, COUNTRY, address.getPostalCountry(), problems);
+      if (isFieldSelected(COUNTRY) && problems.isEmpty()) {
         // Ensure we start with the right language country sub-key.
         String countrySubKey = address.getPostalCountry();
         if (address.getLanguageCode() != null && !address.getLanguageCode().equals("")) {
           countrySubKey += (LOCALE_DELIMITER + address.getLanguageCode());
         }
         v = v.refineVerifier(countrySubKey);
-        verifyField(script, v, ADMIN_AREA, address.getAdministrativeArea(), problems);
-        if (problems.isEmpty()) {
+        verifyFieldIfSelected(script, v, ADMIN_AREA, address.getAdministrativeArea(), problems);
+        if (isFieldSelected(ADMIN_AREA) && problems.isEmpty()) {
           v = v.refineVerifier(address.getAdministrativeArea());
-          verifyField(script, v, LOCALITY, address.getLocality(), problems);
-          if (problems.isEmpty()) {
+          verifyFieldIfSelected(script, v, LOCALITY, address.getLocality(), problems);
+          if (isFieldSelected(LOCALITY) && problems.isEmpty()) {
             v = v.refineVerifier(address.getLocality());
-            verifyField(script, v, DEPENDENT_LOCALITY, address.getDependentLocality(), problems);
-            if (problems.isEmpty()) {
+            verifyFieldIfSelected(
+                script, v, DEPENDENT_LOCALITY, address.getDependentLocality(), problems);
+            if (isFieldSelected(DEPENDENT_LOCALITY) && problems.isEmpty()) {
               v = v.refineVerifier(address.getDependentLocality());
             }
           }
@@ -140,16 +158,32 @@
               address.getAddressLine2());
 
       // Remaining calls don't change the field verifier.
-      verifyField(script, v, POSTAL_CODE, address.getPostalCode(), problems);
-      verifyField(script, v, STREET_ADDRESS, street, problems);
-      verifyField(script, v, SORTING_CODE, address.getSortingCode(), problems);
-      verifyField(script, v, ORGANIZATION, address.getOrganization(), problems);
-      verifyField(script, v, RECIPIENT, address.getRecipient(), problems);
+      verifyFieldIfSelected(script, v, POSTAL_CODE, address.getPostalCode(), problems);
+      verifyFieldIfSelected(script, v, STREET_ADDRESS, street, problems);
+      verifyFieldIfSelected(script, v, SORTING_CODE, address.getSortingCode(), problems);
+      verifyFieldIfSelected(script, v, ORGANIZATION, address.getOrganization(), problems);
+      verifyFieldIfSelected(script, v, RECIPIENT, address.getRecipient(), problems);
 
       postVerify(v, address, problems);
 
       listener.dataLoadingEnd();
     }
+
+    /**
+     * Skips address fields that are not included in {@code addressFieldsToVerify}.
+     */
+    private boolean verifyFieldIfSelected(LookupKey.ScriptType script, FieldVerifier verifier,
+        AddressField field, String value, AddressProblems problems) {
+      if (!isFieldSelected(field)) {
+        return true;
+      }
+
+      return verifyField(script, verifier, field, value, problems);
+    }
+
+    private boolean isFieldSelected(AddressField field) {
+      return addressFieldsToVerify.contains(field);
+    }
   }
 
   /**
diff --git a/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java b/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java
index 2e27ba9..f609c93 100644
--- a/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java
+++ b/common/src/test/java/com/google/i18n/addressinput/common/AddressDataTest.java
@@ -150,4 +150,171 @@
     address = AddressData.builder(address).setLanguageCode("zh-latn").build();
     assertEquals("zh-latn", address.getLanguageCode());
   }
+
+  @Test
+  public void testEqualsIsSymmetric() {
+    AddressData addressData1 = AddressData.builder().build();
+    AddressData addressData2 = AddressData.builder().build();
+
+    assertThat(addressData1).isEqualTo(addressData2);
+    assertThat(addressData2).isEqualTo(addressData1);
+  }
+
+  @Test
+  public void testEqualsIsSymmetricNotEquals() {
+    AddressData addressData1 = AddressData.builder().setCountry("US").build();
+    AddressData addressData2 = AddressData.builder().build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData2).isNotEqualTo(addressData1);
+  }
+
+  @Test
+  public void testEqualsIsTransitive() {
+    AddressData addressData1 = AddressData.builder().build();
+    AddressData addressData2 = AddressData.builder().build();
+    AddressData addressData3 = AddressData.builder().build();
+
+    assertThat(addressData1).isEqualTo(addressData2);
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData1).isEqualTo(addressData3);
+  }
+
+  @Test
+  public void testEqualsIsNullSafe() {
+    AddressData addressData1 = AddressData.builder().build();
+    AddressData addressData2 = null;
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData2).isNotEqualTo(addressData1);
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareCountry() {
+    AddressData addressData1 = AddressData.builder().setCountry("X").build();
+    AddressData addressData2 = AddressData.builder().setCountry("Y").build();
+    AddressData addressData3 = AddressData.builder().setCountry("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareAddressLines() {
+    AddressData addressData1 = AddressData.builder().setAddress("X").build();
+    AddressData addressData2 = AddressData.builder().setAddress("Y").build();
+    AddressData addressData3 = AddressData.builder().setAddress("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareAdminArea() {
+    AddressData addressData1 = AddressData.builder().setAdminArea("X").build();
+    AddressData addressData2 = AddressData.builder().setAdminArea("Y").build();
+    AddressData addressData3 = AddressData.builder().setAdminArea("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareLocality() {
+    AddressData addressData1 = AddressData.builder().setLocality("X").build();
+    AddressData addressData2 = AddressData.builder().setLocality("Y").build();
+    AddressData addressData3 = AddressData.builder().setLocality("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareDependentLocality() {
+    AddressData addressData1 = AddressData.builder().setDependentLocality("X").build();
+    AddressData addressData2 = AddressData.builder().setDependentLocality("Y").build();
+    AddressData addressData3 = AddressData.builder().setDependentLocality("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeComparePostalCode() {
+    AddressData addressData1 = AddressData.builder().setPostalCode("X").build();
+    AddressData addressData2 = AddressData.builder().setPostalCode("Y").build();
+    AddressData addressData3 = AddressData.builder().setPostalCode("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareSortingCode() {
+    AddressData addressData1 = AddressData.builder().setSortingCode("X").build();
+    AddressData addressData2 = AddressData.builder().setSortingCode("Y").build();
+    AddressData addressData3 = AddressData.builder().setSortingCode("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareOrganization() {
+    AddressData addressData1 = AddressData.builder().setOrganization("X").build();
+    AddressData addressData2 = AddressData.builder().setOrganization("Y").build();
+    AddressData addressData3 = AddressData.builder().setOrganization("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareRecipient() {
+    AddressData addressData1 = AddressData.builder().setRecipient("X").build();
+    AddressData addressData2 = AddressData.builder().setRecipient("Y").build();
+    AddressData addressData3 = AddressData.builder().setRecipient("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
+
+  @Test
+  public void testEqualsAndHashCodeCompareLanguageCode() {
+    AddressData addressData1 = AddressData.builder().setLanguageCode("X").build();
+    AddressData addressData2 = AddressData.builder().setLanguageCode("Y").build();
+    AddressData addressData3 = AddressData.builder().setLanguageCode("Y").build();
+
+    assertThat(addressData1).isNotEqualTo(addressData2);
+    assertThat(addressData1.hashCode()).isNotEqualTo(addressData2.hashCode());
+
+    assertThat(addressData2).isEqualTo(addressData3);
+    assertThat(addressData2.hashCode()).isEqualTo(addressData3.hashCode());
+  }
 }
diff --git a/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java b/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java
index f89debf..32e8e79 100644
--- a/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java
+++ b/common/src/test/java/com/google/i18n/addressinput/common/StandardAddressVerifierTest.java
@@ -17,6 +17,8 @@
 package com.google.i18n.addressinput.common;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.i18n.addressinput.common.AddressField.ADMIN_AREA;
+import static com.google.i18n.addressinput.common.AddressField.COUNTRY;
 import static com.google.i18n.addressinput.common.AddressField.DEPENDENT_LOCALITY;
 import static com.google.i18n.addressinput.common.AddressField.LOCALITY;
 import static com.google.i18n.addressinput.common.AddressField.POSTAL_CODE;
@@ -34,7 +36,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 
@@ -259,4 +261,52 @@
         POSTAL_CODE, MISSING_REQUIRED_FIELD),
         verify(address).getProblems());
   }
+
+  @Test public void testVerifyCountryOnly_Valid() {
+    AddressData address = AddressData.builder()
+        .setCountry("US")
+        .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+        .build();
+    AddressProblems problems = new AddressProblems();
+    verifierFor(StandardChecks.PROBLEM_MAP)
+        .verifyFields(address, problems, EnumSet.of(COUNTRY));
+    assertThat(problems.getProblems()).isEmpty();
+  }
+
+  @Test public void testVerifyCountryOnly_InvalidCountry() {
+    AddressData address = AddressData.builder()
+        .setCountry("USA")
+        .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+        .build();
+    AddressProblems problems = new AddressProblems();
+    verifierFor(StandardChecks.PROBLEM_MAP)
+        .verifyFields(address, problems, EnumSet.of(COUNTRY));
+    assertThat(problems.getProblem(COUNTRY)).isEqualTo(UNKNOWN_VALUE);
+    assertThat(problems.getProblem(ADMIN_AREA)).isNull();
+  }
+
+  @Test public void testVerifyCountryAndPostalCodeOnly_Valid() {
+    AddressData address = AddressData.builder()
+        .setCountry("US")
+        .setPostalCode("94043")
+        .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+        .build();
+    AddressProblems problems = new AddressProblems();
+    verifierFor(StandardChecks.PROBLEM_MAP)
+        .verifyFields(address, problems, EnumSet.of(COUNTRY, POSTAL_CODE));
+    assertThat(problems.getProblems()).isEmpty();
+  }
+
+  @Test public void testVerifyCountryAndPostalCodeOnly_InvalidPostalCode() {
+    AddressData address = AddressData.builder()
+        .setCountry("US")
+        .setPostalCode("094043")
+        .setAdminArea("Invalid admin area") // Non-selected field should be ignored
+        .build();
+    AddressProblems problems = new AddressProblems();
+    verifierFor(StandardChecks.PROBLEM_MAP)
+        .verifyFields(address, problems, EnumSet.of(COUNTRY, POSTAL_CODE));
+    assertThat(problems.getProblem(POSTAL_CODE)).isEqualTo(INVALID_FORMAT);
+    assertThat(problems.getProblem(ADMIN_AREA)).isNull();
+  }
 }
diff --git a/cpp/README b/cpp/README.md
similarity index 97%
rename from cpp/README
rename to cpp/README.md
index d6fbf44..37050d0 100644
--- a/cpp/README
+++ b/cpp/README.md
@@ -1,5 +1,4 @@
-Intro
-=====
+# Intro
 
 The C++ version of libaddressinput library provides UI layout information and
 validation for address input forms.
@@ -15,8 +14,7 @@
 and include directories in libaddressinput.gypi to link with your own
 third-party libraries.
 
-Dependencies
-============
+# Dependencies
 
 The library depends on these tools and libraries:
 
@@ -59,8 +57,7 @@
 http://python.org/
 https://code.google.com/p/re2/
 
-Build
-=====
+# Build
 
 Building the library involves generating an out/Default/build.ninja file and
 running ninja:
@@ -74,8 +71,7 @@
 
 $ export GYP_DEFINES="gtest_dir='/xxx/include' gtest_src_dir='/xxx'"
 
-Test
-====
+# Test
 
 This command will execute the unit tests for the library:
 
diff --git a/cpp/include/libaddressinput/util/basictypes.h b/cpp/include/libaddressinput/util/basictypes.h
index 663133f..4436f5a 100644
--- a/cpp/include/libaddressinput/util/basictypes.h
+++ b/cpp/include/libaddressinput/util/basictypes.h
@@ -108,9 +108,16 @@
 // A macro to disallow the copy constructor and operator= functions
 // This should be used in the private: declarations for a class
 #if !defined(DISALLOW_COPY_AND_ASSIGN)
+#if __cplusplus >= 201103L
+// Use C++11 deleted destructor since they provide better error messages.
+#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
+  TypeName(const TypeName&) = delete;      \
+  TypeName& operator=(const TypeName&) = delete
+#else  // Not C++11
 #define DISALLOW_COPY_AND_ASSIGN(TypeName) \
   TypeName(const TypeName&);               \
   void operator=(const TypeName&)
+#endif  // Not C++11
 #endif
 
 // The arraysize(arr) macro returns the # of elements in an array arr.
@@ -120,7 +127,7 @@
 //
 // One caveat is that arraysize() doesn't accept any array of an
 // anonymous type or a type defined inside a function.  In these rare
-// cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below.  This is
+// cases, you have to use the unsafe ARRAYSIZE() macro below.  This is
 // due to a limitation in C++'s template system.  The limitation might
 // eventually be removed, but it hasn't happened yet.
 
@@ -142,26 +149,26 @@
 #define arraysize(array) (sizeof(ArraySizeHelper(array)))
 #endif
 
-// ARRAYSIZE_UNSAFE performs essentially the same calculation as arraysize,
+// ARRAYSIZE performs essentially the same calculation as arraysize,
 // but can be used on anonymous types or types defined inside
 // functions.  It's less safe than arraysize as it accepts some
 // (although not all) pointers.  Therefore, you should use arraysize
 // whenever possible.
 //
-// The expression ARRAYSIZE_UNSAFE(a) is a compile-time constant of type
+// The expression ARRAYSIZE(a) is a compile-time constant of type
 // size_t.
 //
-// ARRAYSIZE_UNSAFE catches a few type errors.  If you see a compiler error
+// ARRAYSIZE catches a few type errors.  If you see a compiler error
 //
 //   "warning: division by zero in ..."
 //
-// when using ARRAYSIZE_UNSAFE, you are (wrongfully) giving it a pointer.
-// You should only use ARRAYSIZE_UNSAFE on statically allocated arrays.
+// when using ARRAYSIZE, you are (wrongfully) giving it a pointer.
+// You should only use ARRAYSIZE on statically allocated arrays.
 //
 // The following comments are on the implementation details, and can
 // be ignored by the users.
 //
-// ARRAYSIZE_UNSAFE(arr) works by inspecting sizeof(arr) (the # of bytes in
+// ARRAYSIZE(arr) works by inspecting sizeof(arr) (the # of bytes in
 // the array) and sizeof(*(arr)) (the # of bytes in one array
 // element).  If the former is divisible by the latter, perhaps arr is
 // indeed an array, in which case the division result is the # of
@@ -179,8 +186,8 @@
 // where a pointer is 4 bytes, this means all pointers to a type whose
 // size is 3 or greater than 4 will be (righteously) rejected.
 
-#if !defined(ARRAYSIZE_UNSAFE)
-#define ARRAYSIZE_UNSAFE(a) \
+#if !defined(ARRAYSIZE)
+#define ARRAYSIZE(a) \
   ((sizeof(a) / sizeof(*(a))) / \
    static_cast<size_t>(!(sizeof(a) % sizeof(*(a)))))
 #endif
@@ -189,7 +196,7 @@
 // expression is true. For example, you could use it to verify the
 // size of a static array:
 //
-//   COMPILE_ASSERT(ARRAYSIZE_UNSAFE(content_type_names) == CONTENT_NUM_TYPES,
+//   COMPILE_ASSERT(ARRAYSIZE(content_type_names) == CONTENT_NUM_TYPES,
 //                  content_type_names_incorrect_size);
 //
 // or to make sure a struct is smaller than a certain size:
@@ -200,14 +207,23 @@
 // the expression is false, most compilers will issue a warning/error
 // containing the name of the variable.
 
+#if !defined(COMPILE_ASSERT)
+
+#if __cplusplus >= 201103L
+// Use static_assert() directly when using a C++11 compiler.
+// This provides human-friendly error messages.
+#define COMPILE_ASSERT(expr, msg) static_assert((expr), #msg)
+#else  // Not C++11
+// Otherwise, use a compile-time type error.
 template <bool>
 struct CompileAssert {
 };
 
-#if !defined(COMPILE_ASSERT)
 #define COMPILE_ASSERT(expr, msg) \
   typedef CompileAssert<(bool(expr))> msg[bool(expr) ? 1 : -1]
-#endif
+
+#endif  // Not C++11
+#endif  // !defined(COMPILE_ASSERT)
 
 #endif  // I18N_ADDRESSINPUT_UTIL_BASICTYPES_H_
 #endif  // I18N_ADDRESSINPUT_USE_BASICTYPES_OVERRIDE
diff --git a/cpp/src/address_field_util.cc b/cpp/src/address_field_util.cc
index 26de3c0..3c2b7cd 100644
--- a/cpp/src/address_field_util.cc
+++ b/cpp/src/address_field_util.cc
@@ -15,13 +15,12 @@
 #include "address_field_util.h"
 
 #include <libaddressinput/address_field.h>
+#include <libaddressinput/util/basictypes.h>
 
 #include <algorithm>
 #include <cassert>
 #include <cstddef>
-#include <map>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "format_element.h"
@@ -31,33 +30,33 @@
 
 namespace {
 
-std::map<char, AddressField> InitFields() {
-  std::map<char, AddressField> fields;
-  fields.insert(std::make_pair('R', COUNTRY));
-  fields.insert(std::make_pair('S', ADMIN_AREA));
-  fields.insert(std::make_pair('C', LOCALITY));
-  fields.insert(std::make_pair('D', DEPENDENT_LOCALITY));
-  fields.insert(std::make_pair('X', SORTING_CODE));
-  fields.insert(std::make_pair('Z', POSTAL_CODE));
-  fields.insert(std::make_pair('A', STREET_ADDRESS));
-  fields.insert(std::make_pair('O', ORGANIZATION));
-  fields.insert(std::make_pair('N', RECIPIENT));
-  return fields;
-}
+// Check whether |c| is a field token character. On success, return true
+// and sets |*field| to the corresponding AddressField value. Return false
+// on failure.
+bool ParseFieldToken(char c, AddressField* field) {
+  assert(field != NULL);
 
-const std::map<char, AddressField>& GetFields() {
-  static const std::map<char, AddressField> kFields(InitFields());
-  return kFields;
-}
+  // Simple mapping from field token characters to AddressField values.
+  static const struct Entry { char ch; AddressField field; } kTokenMap[] = {
+    { 'R', COUNTRY },
+    { 'S', ADMIN_AREA },
+    { 'C', LOCALITY },
+    { 'D', DEPENDENT_LOCALITY },
+    { 'X', SORTING_CODE },
+    { 'Z', POSTAL_CODE },
+    { 'A', STREET_ADDRESS },
+    { 'O', ORGANIZATION },
+    { 'N', RECIPIENT },
+  };
+  const size_t kTokenMapSize = arraysize(kTokenMap);
 
-bool IsFieldToken(char c) {
-  return GetFields().find(c) != GetFields().end();
-}
-
-AddressField ParseFieldToken(char c) {
-  std::map<char, AddressField>::const_iterator it = GetFields().find(c);
-  assert(it != GetFields().end());
-  return it->second;
+  for (size_t n = 0; n < kTokenMapSize; ++n) {
+      if (c == kTokenMap[n].ch) {
+          *field = kTokenMap[n].field;
+          return true;
+      }
+  }
+  return false;
 }
 
 }  // namespace
@@ -85,10 +84,11 @@
       break;
     }
     // Process the token after the %.
+    AddressField field;
     if (*next == 'n') {
       elements->push_back(FormatElement());
-    } else if (IsFieldToken(*next)) {
-      elements->push_back(FormatElement(ParseFieldToken(*next)));
+    } else if (ParseFieldToken(*next, &field)) {
+      elements->push_back(FormatElement(field));
     }  // Else it's an unknown token, we ignore it.
   }
   // Push back any trailing literal.
@@ -103,8 +103,9 @@
   fields->clear();
   for (std::string::const_iterator it = required.begin();
        it != required.end(); ++it) {
-    if (IsFieldToken(*it)) {
-      fields->push_back(ParseFieldToken(*it));
+    AddressField field;
+    if (ParseFieldToken(*it, &field)) {
+      fields->push_back(field);
     }
   }
 }
diff --git a/cpp/src/region_data_constants.cc b/cpp/src/region_data_constants.cc
index ca1ee1b..738cd07 100644
--- a/cpp/src/region_data_constants.cc
+++ b/cpp/src/region_data_constants.cc
@@ -58,8 +58,7 @@
   region_data.insert(std::make_pair("AF", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"zipex\":\"1001,2601,3801\","
-      "\"posturl\":\"http://afghanpost.gov.af/Postal%20Code/\","
-      "\"languages\":\"fa~ps\""
+      "\"languages\":\"fa~ps~uz-Arab~tk~bal\""
       "}"));
   region_data.insert(std::make_pair("AG", "{"
       "\"require\":\"A\","
@@ -106,7 +105,7 @@
       "\"require\":\"ACZ\","
       "\"zipex\":\"1010,3741\","
       "\"posturl\":\"http://www.post.at/post_subsite_postleitzahlfinder.php\","
-      "\"languages\":\"de\""
+      "\"languages\":\"de~hr~sl~hu\""
       "}"));
   region_data.insert(std::make_pair("AU", "{"
       "\"fmt\":\"%O%n%N%n%A%n%C %S %Z\","
@@ -130,12 +129,12 @@
   region_data.insert(std::make_pair("AZ", "{"
       "\"fmt\":\"%N%n%O%n%A%nAZ %Z %C\","
       "\"zipex\":\"1000\","
-      "\"languages\":\"az-Latn~az-Cyrl\""
+      "\"languages\":\"az~az-Cyrl\""
       "}"));
   region_data.insert(std::make_pair("BA", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"71000\","
-      "\"languages\":\"bs-Cyrl~bs-Latn~hr~sr-Cyrl~sr-Latn\""
+      "\"languages\":\"bs~bs-Cyrl~hr~sr~sr-Latn\""
       "}"));
   region_data.insert(std::make_pair("BB", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\","
@@ -193,8 +192,8 @@
   region_data.insert(std::make_pair("BN", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C %Z\","
       "\"zipex\":\"BT2328,KA1131,BA1511\","
-      "\"posturl\":\"http://www.post.gov.bn/index.php/extensions/postcode-guide\","
-      "\"languages\":\"ms-Latn~ms-Arab\""
+      "\"posturl\":\"http://www.post.gov.bn/SitePages/postcodes.aspx\","
+      "\"languages\":\"ms~ms-Arab\""
       "}"));
   region_data.insert(std::make_pair("BO", "{"
       "\"languages\":\"es~qu~ay\""
@@ -240,7 +239,7 @@
       "\"fmt\":\"%N%n%O%n%A%n%C %S %Z\","
       "\"require\":\"ACSZ\","
       "\"zipex\":\"H3Z 2Y7,V8X 3X4,T0L 1K0,T0H 1A0,K1A 0B1\","
-      "\"posturl\":\"http://www.canadapost.ca/cpotools/apps/fpc/personal/findByCity\?execution=e2s1\","
+      "\"posturl\":\"https://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf\","
       "\"languages\":\"en~fr\""
       "}"));
   region_data.insert(std::make_pair("CC", "{"
@@ -249,7 +248,7 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("CD", "{"
-      "\"languages\":\"fr\""
+      "\"languages\":\"sw~lua~fr~ln~kg\""
       "}"));
   region_data.insert(std::make_pair("CF", "{"
       "\"languages\":\"fr~sg\""
@@ -262,7 +261,7 @@
       "\"require\":\"ACZ\","
       "\"zipex\":\"2544,1211,1556,3030\","
       "\"posturl\":\"http://www.post.ch/db/owa/pv_plz_pack/pr_main\","
-      "\"languages\":\"de~gsw~fr~it\""
+      "\"languages\":\"de~gsw~fr~it~rm\""
       "}"));
   region_data.insert(std::make_pair("CI", "{"
       "\"fmt\":\"%N%n%O%n%X %A %C %X\","
@@ -344,7 +343,7 @@
       "\"require\":\"ACZ\","
       "\"zipex\":\"8660,1566\","
       "\"posturl\":\"http://www.postdanmark.dk/da/Privat/Kundeservice/postnummerkort/Sider/Find-postnummer.aspx\","
-      "\"languages\":\"da\""
+      "\"languages\":\"da~de~kl\""
       "}"));
   region_data.insert(std::make_pair("DM", "{"
       "\"languages\":\"en\""
@@ -391,7 +390,7 @@
       "\"require\":\"ACSZ\","
       "\"zipex\":\"28039,28300,28070\","
       "\"posturl\":\"http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp\","
-      "\"languages\":\"es\""
+      "\"languages\":\"es~ca~gl~eu\""
       "}"));
   region_data.insert(std::make_pair("ET", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -406,7 +405,7 @@
       "\"languages\":\"fi~sv\""
       "}"));
   region_data.insert(std::make_pair("FJ", "{"
-      "\"languages\":\"en~hif-Latn~fj\""
+      "\"languages\":\"en~hif~fj\""
       "}"));
   region_data.insert(std::make_pair("FK", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
@@ -445,7 +444,7 @@
       "\"locality_name_type\":\"post_town\","
       "\"zipex\":\"EC1Y 8SY,GIR 0AA,M2 5BQ,M34 4AB,CR0 2YR,DN16 9AA,W1A 4ZZ,EC1A 1HQ,OX14 4PG,BS18 8HF,NR25 7HG,RH6 0NP,BH23 6AA,B6 5BA,SO23 9AP,PO1 3AX,BFPO 61\","
       "\"posturl\":\"http://www.royalmail.com/postcode-finder\","
-      "\"languages\":\"en\""
+      "\"languages\":\"en~cy~gd~ga\""
       "}"));
   region_data.insert(std::make_pair("GD", "{"
       "\"languages\":\"en\""
@@ -454,7 +453,7 @@
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"0101\","
       "\"posturl\":\"http://www.georgianpost.ge/index.php\?page=10\","
-      "\"languages\":\"ka\""
+      "\"languages\":\"ka~ab~os\""
       "}"));
   region_data.insert(std::make_pair("GF", "{"
       "\"fmt\":\"%O%n%N%n%A%n%Z %C %X\","
@@ -471,7 +470,7 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("GH", "{"
-      "\"languages\":\"en\""
+      "\"languages\":\"ak~en~ee~gaa\""
       "}"));
   region_data.insert(std::make_pair("GI", "{"
       "\"fmt\":\"%N%n%O%n%A%nGIBRALTAR%n%Z\","
@@ -518,13 +517,12 @@
   region_data.insert(std::make_pair("GT", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z- %C\","
       "\"zipex\":\"09001,01501\","
-      "\"languages\":\"es\""
+      "\"languages\":\"es~quc\""
       "}"));
   region_data.insert(std::make_pair("GU", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%C %S %Z\","
-      "\"require\":\"ACSZ\","
+      "\"fmt\":\"%N%n%O%n%A%n%C %Z\","
+      "\"require\":\"ACZ\","
       "\"zip_name_type\":\"zip\","
-      "\"state_name_type\":\"state\","
       "\"zipex\":\"96910,96931\","
       "\"posturl\":\"http://zip4.usps.com/zip4/welcome.jsp\","
       "\"languages\":\"en~ch\""
@@ -559,7 +557,7 @@
       "\"fmt\":\"%N%n%O%n%A%nHR-%Z %C\","
       "\"zipex\":\"10000,21001,10002\","
       "\"posturl\":\"http://www.posta.hr/default.aspx\?pretpum\","
-      "\"languages\":\"hr\""
+      "\"languages\":\"hr~it\""
       "}"));
   region_data.insert(std::make_pair("HT", "{"
       "\"fmt\":\"%N%n%O%n%A%nHT%Z %C\","
@@ -607,8 +605,8 @@
       "\"zip_name_type\":\"pin\","
       "\"state_name_type\":\"state\","
       "\"zipex\":\"110034,110001\","
-      "\"posturl\":\"http://cept.gov.in/lbpsd/placesearch.aspx\","
-      "\"languages\":\"en\""
+      "\"posturl\":\"https://www.indiapost.gov.in/vas/pages/FindPinCode.aspx\","
+      "\"languages\":\"en~hi\""
       "}"));
   region_data.insert(std::make_pair("IO", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
@@ -620,7 +618,7 @@
       "\"fmt\":\"%O%n%N%n%A%n%C, %S%n%Z\","
       "\"require\":\"ACS\","
       "\"zipex\":\"31001\","
-      "\"languages\":\"ar\""
+      "\"languages\":\"ar~ckb~az-Arab\""
       "}"));
   region_data.insert(std::make_pair("IR", "{"
       "\"fmt\":\"%O%n%N%n%S%n%C, %D%n%A%n%Z\","
@@ -631,7 +629,7 @@
   region_data.insert(std::make_pair("IS", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"320,121,220,110\","
-      "\"posturl\":\"https://www.postur.is/um-postinn/posthus/gotuskra/\","
+      "\"posturl\":\"http://www.postur.is/einstaklingar/posthus/postnumer/\","
       "\"languages\":\"is\""
       "}"));
   region_data.insert(std::make_pair("IT", "{"
@@ -676,7 +674,7 @@
   region_data.insert(std::make_pair("KG", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"720001\","
-      "\"languages\":\"ky-Cyrl~ru\""
+      "\"languages\":\"ky~ru\""
       "}"));
   region_data.insert(std::make_pair("KH", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C %Z\","
@@ -723,7 +721,7 @@
   region_data.insert(std::make_pair("KZ", "{"
       "\"fmt\":\"%Z%n%S%n%C%n%A%n%O%n%N\","
       "\"zipex\":\"040900,050012\","
-      "\"languages\":\"ru~kk-Cyrl\""
+      "\"languages\":\"ru~kk\""
       "}"));
   region_data.insert(std::make_pair("LA", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -786,7 +784,7 @@
   region_data.insert(std::make_pair("MA", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"53000,10000,20050,16052\","
-      "\"languages\":\"ar~fr~tzm-Latn\""
+      "\"languages\":\"ar~fr~tzm\""
       "}"));
   region_data.insert(std::make_pair("MC", "{"
       "\"fmt\":\"%N%n%O%n%A%nMC-%Z %C %X\","
@@ -827,7 +825,7 @@
   region_data.insert(std::make_pair("MK", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"1314,1321,1443,1062\","
-      "\"languages\":\"mk\""
+      "\"languages\":\"mk~sq\""
       "}"));
   region_data.insert(std::make_pair("ML", "{"
       "\"languages\":\"fr\""
@@ -841,7 +839,7 @@
       "\"fmt\":\"%N%n%O%n%A%n%C%n%S %Z\","
       "\"zipex\":\"65030,65270\","
       "\"posturl\":\"http://www.zipcode.mn/\","
-      "\"languages\":\"mn-Cyrl\""
+      "\"languages\":\"mn\""
       "}"));
   region_data.insert(std::make_pair("MO", "{"
       "\"fmt\":\"%A%n%O%n%N\","
@@ -936,7 +934,7 @@
       "\"languages\":\"en\""
       "}"));
   region_data.insert(std::make_pair("NG", "{"
-      "\"fmt\":\"%N%n%O%n%A%n%C %Z%n%S\","
+      "\"fmt\":\"%N%n%O%n%A%n%D%n%C %Z%n%S\","
       "\"state_name_type\":\"state\","
       "\"zipex\":\"930283,300001,931104\","
       "\"posturl\":\"http://www.nigeriapostcodes.com/\","
@@ -954,7 +952,7 @@
       "\"require\":\"ACZ\","
       "\"zipex\":\"1234 AB,2490 AA\","
       "\"posturl\":\"http://www.postnl.nl/voorthuis/\","
-      "\"languages\":\"nl\""
+      "\"languages\":\"nl~fy\""
       "}"));
   region_data.insert(std::make_pair("NO", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -962,7 +960,7 @@
       "\"locality_name_type\":\"post_town\","
       "\"zipex\":\"0025,0107,6631\","
       "\"posturl\":\"http://adressesok.posten.no/nb/postal_codes/search\","
-      "\"languages\":\"no~nn\""
+      "\"languages\":\"no~nn~se\""
       "}"));
   region_data.insert(std::make_pair("NP", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C %Z\","
@@ -1031,7 +1029,7 @@
       "\"require\":\"ACZ\","
       "\"zipex\":\"00-950,05-470,48-300,32-015,00-940\","
       "\"posturl\":\"http://kody.poczta-polska.pl/\","
-      "\"languages\":\"pl\""
+      "\"languages\":\"pl~de~csb~lt\""
       "}"));
   region_data.insert(std::make_pair("PM", "{"
       "\"fmt\":\"%O%n%N%n%A%n%Z %C %X\","
@@ -1097,7 +1095,7 @@
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"106314\","
       "\"posturl\":\"http://www.posta.rs/struktura/lat/aplikacije/pronadji/nadji-postu.asp\","
-      "\"languages\":\"sr-Cyrl~sr-Latn\""
+      "\"languages\":\"sr~sr-Latn~hu~ro~hr~sk~uk\""
       "}"));
   region_data.insert(std::make_pair("RU", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C%n%S%n%Z\","
@@ -1130,14 +1128,14 @@
       "\"locality_name_type\":\"post_town\","
       "\"zipex\":\"11455,12345,10500\","
       "\"posturl\":\"http://www.posten.se/sv/Kundservice/Sidor/Sok-postnummer-resultat.aspx\","
-      "\"languages\":\"sv\""
+      "\"languages\":\"sv~fi\""
       "}"));
   region_data.insert(std::make_pair("SG", "{"
       "\"fmt\":\"%N%n%O%n%A%nSINGAPORE %Z\","
       "\"require\":\"AZ\","
       "\"zipex\":\"546080,308125,408600\","
       "\"posturl\":\"https://www.singpost.com/find-postal-code\","
-      "\"languages\":\"en~zh-Hans~ms-Latn~ta\""
+      "\"languages\":\"en~zh~ms~ta\""
       "}"));
   region_data.insert(std::make_pair("SH", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
@@ -1177,7 +1175,7 @@
   region_data.insert(std::make_pair("SN", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"12500,46024,16556,10000\","
-      "\"languages\":\"wo~fr\""
+      "\"languages\":\"wo~fr~ff~srr~dyo~sav~mfv~bjt~snf~knf~bsc~mey~tnr\""
       "}"));
   region_data.insert(std::make_pair("SO", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C, %S %Z\","
@@ -1207,7 +1205,7 @@
   region_data.insert(std::make_pair("SZ", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C%n%Z\","
       "\"zipex\":\"H100\","
-      "\"posturl\":\"http://www.sptc.co.sz/swazipost/codes.php\","
+      "\"posturl\":\"http://www.sptc.co.sz/swazipost/codes/index.php\","
       "\"languages\":\"en~ss\""
       "}"));
   region_data.insert(std::make_pair("TA", "{"
@@ -1239,7 +1237,7 @@
   region_data.insert(std::make_pair("TJ", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"735450,734025\","
-      "\"languages\":\"tg-Cyrl\""
+      "\"languages\":\"tg\""
       "}"));
   region_data.insert(std::make_pair("TK", "{"
       "\"languages\":\"en~tkl\""
@@ -1250,7 +1248,7 @@
   region_data.insert(std::make_pair("TM", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"744000\","
-      "\"languages\":\"tk-Latn\""
+      "\"languages\":\"tk\""
       "}"));
   region_data.insert(std::make_pair("TN", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
@@ -1331,12 +1329,12 @@
       "\"fmt\":\"%N%n%O%n%A%n%Z %C%n%S\","
       "\"zipex\":\"702100,700000\","
       "\"posturl\":\"http://www.pochta.uz/ru/uslugi/indexsearch.html\","
-      "\"languages\":\"uz-Latn~uz-Cyrl\""
+      "\"languages\":\"uz~uz-Cyrl\""
       "}"));
   region_data.insert(std::make_pair("VA", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"00120\","
-      "\"languages\":\"it~la\""
+      "\"languages\":\"it\""
       "}"));
   region_data.insert(std::make_pair("VC", "{"
       "\"fmt\":\"%N%n%O%n%A%n%C %Z\","
@@ -1389,7 +1387,7 @@
   region_data.insert(std::make_pair("XK", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
       "\"zipex\":\"10000\","
-      "\"languages\":\"sq~sr-Cyrl~sr-Latn\""
+      "\"languages\":\"sq~sr~sr-Latn\""
       "}"));
   region_data.insert(std::make_pair("YE", "{"
       "\"languages\":\"ar\""
@@ -1405,7 +1403,7 @@
       "\"require\":\"ACZ\","
       "\"zipex\":\"0083,1451,0001\","
       "\"posturl\":\"https://www.postoffice.co.za/contactus/postalcode.html\","
-      "\"languages\":\"en\""
+      "\"languages\":\"en~zu~xh~af~nso~tn~st~ts~ss~ve~nr\""
       "}"));
   region_data.insert(std::make_pair("ZM", "{"
       "\"fmt\":\"%N%n%O%n%A%n%Z %C\","
diff --git a/cpp/src/rule.cc b/cpp/src/rule.cc
index 550ac2e..91427d0 100644
--- a/cpp/src/rule.cc
+++ b/cpp/src/rule.cc
@@ -14,9 +14,9 @@
 
 #include "rule.h"
 
+#include <algorithm>
 #include <cassert>
 #include <cstddef>
-#include <map>
 #include <string>
 #include <utility>
 
@@ -36,110 +36,123 @@
 
 namespace {
 
-typedef std::map<std::string, int> NameMessageIdMap;
-
 // Used as a separator in a list of items. For example, the list of supported
 // languages can be "de~fr~it".
 const char kSeparator = '~';
 
-NameMessageIdMap InitAdminAreaMessageIds() {
-  NameMessageIdMap message_ids;
-  message_ids.insert(std::make_pair(
-      "area", IDS_LIBADDRESSINPUT_AREA));
-  message_ids.insert(std::make_pair(
-      "county", IDS_LIBADDRESSINPUT_COUNTY));
-  message_ids.insert(std::make_pair(
-      "department", IDS_LIBADDRESSINPUT_DEPARTMENT));
-  message_ids.insert(std::make_pair(
-      "district", IDS_LIBADDRESSINPUT_DISTRICT));
-  message_ids.insert(std::make_pair(
-      "do_si", IDS_LIBADDRESSINPUT_DO_SI));
-  message_ids.insert(std::make_pair(
-      "emirate", IDS_LIBADDRESSINPUT_EMIRATE));
-  message_ids.insert(std::make_pair(
-      "island", IDS_LIBADDRESSINPUT_ISLAND));
-  message_ids.insert(std::make_pair(
-      "oblast", IDS_LIBADDRESSINPUT_OBLAST));
-  message_ids.insert(std::make_pair(
-      "parish", IDS_LIBADDRESSINPUT_PARISH));
-  message_ids.insert(std::make_pair(
-      "prefecture", IDS_LIBADDRESSINPUT_PREFECTURE));
-  message_ids.insert(std::make_pair(
-      "province", IDS_LIBADDRESSINPUT_PROVINCE));
-  message_ids.insert(std::make_pair(
-      "state", IDS_LIBADDRESSINPUT_STATE));
-  return message_ids;
-}
+// NameIdMap is a convenience POD struct that implements a mapping from
+// names to message ids, with sorted arrays of NameIdInfo entries.
+struct NameIdInfo {
+  const char* name;
+  int id;
 
-const NameMessageIdMap& GetAdminAreaMessageIds() {
-  static const NameMessageIdMap kAdminAreaMessageIds(InitAdminAreaMessageIds());
-  return kAdminAreaMessageIds;
-}
+  static bool less(const NameIdInfo& a, const NameIdInfo& b) {
+    return strcmp(a.name, b.name) < 0;
+  }
+};
 
-NameMessageIdMap InitPostalCodeMessageIds() {
-  NameMessageIdMap message_ids;
-  message_ids.insert(std::make_pair(
-      "eircode", IDS_LIBADDRESSINPUT_EIR_CODE_LABEL));
-  message_ids.insert(std::make_pair(
-      "pin", IDS_LIBADDRESSINPUT_PIN_CODE_LABEL));
-  message_ids.insert(std::make_pair(
-      "postal", IDS_LIBADDRESSINPUT_POSTAL_CODE_LABEL));
-  message_ids.insert(std::make_pair(
-      "zip", IDS_LIBADDRESSINPUT_ZIP_CODE_LABEL));
-  return message_ids;
-}
+struct NameIdMap {
+  const NameIdInfo* infos;
+  size_t size;
 
-const NameMessageIdMap& GetPostalCodeMessageIds() {
-  static const NameMessageIdMap kPostalCodeMessageIds(
-      InitPostalCodeMessageIds());
-  return kPostalCodeMessageIds;
-}
+  // Return the message id corresponding to |name|, ir INVALID_MESSAGE_ID
+  // if it is not found in the map.
+  int GetIdFromName(const std::string& name) const {
+    NameIdInfo key = { name.c_str() };
+    const NameIdInfo* begin = infos;
+    const NameIdInfo* end = begin + size;
+    const NameIdInfo* probe =
+        std::lower_bound(begin, end, key, NameIdInfo::less);
+    return (probe != end && name == probe->name)
+        ? probe->id : INVALID_MESSAGE_ID;
+  }
 
-NameMessageIdMap InitLocalityMessageIds() {
-  NameMessageIdMap message_ids;
-  message_ids.insert(std::make_pair(
-      "city", IDS_LIBADDRESSINPUT_LOCALITY_LABEL));
-  message_ids.insert(std::make_pair(
-      "district", IDS_LIBADDRESSINPUT_DISTRICT));
-  message_ids.insert(std::make_pair(
-      "post_town", IDS_LIBADDRESSINPUT_POST_TOWN));
-  message_ids.insert(std::make_pair(
-      "suburb", IDS_LIBADDRESSINPUT_SUBURB));
-  return message_ids;
-}
+  // Return true iff the map is properly sorted.
+  bool IsSorted() const {
+    for (size_t n = 1; n < size; ++n) {
+      if (!NameIdInfo::less(infos[n - 1], infos[n])) {
+        return false;
+      }
+    }
+    return true;
+  }
+};
 
-const NameMessageIdMap& GetLocalityMessageIds() {
-  static const NameMessageIdMap kLocalityMessageIds(
-      InitLocalityMessageIds());
-  return kLocalityMessageIds;
-}
+const NameIdInfo kAdminAreaInfoArray[] = {
+  {"area", IDS_LIBADDRESSINPUT_AREA},
+  {"county", IDS_LIBADDRESSINPUT_COUNTY},
+  {"department", IDS_LIBADDRESSINPUT_DEPARTMENT},
+  {"district", IDS_LIBADDRESSINPUT_DISTRICT},
+  {"do_si", IDS_LIBADDRESSINPUT_DO_SI},
+  {"emirate", IDS_LIBADDRESSINPUT_EMIRATE},
+  {"island", IDS_LIBADDRESSINPUT_ISLAND},
+  {"oblast", IDS_LIBADDRESSINPUT_OBLAST},
+  {"parish", IDS_LIBADDRESSINPUT_PARISH},
+  {"prefecture", IDS_LIBADDRESSINPUT_PREFECTURE},
+  {"province", IDS_LIBADDRESSINPUT_PROVINCE},
+  {"state", IDS_LIBADDRESSINPUT_STATE},
+};
 
-NameMessageIdMap InitSublocalityMessageIds() {
-  NameMessageIdMap message_ids;
-  message_ids.insert(std::make_pair(
-      "suburb", IDS_LIBADDRESSINPUT_SUBURB));
-  message_ids.insert(std::make_pair(
-      "district", IDS_LIBADDRESSINPUT_DISTRICT));
-  message_ids.insert(std::make_pair(
-      "neighborhood", IDS_LIBADDRESSINPUT_NEIGHBORHOOD));
-  message_ids.insert(std::make_pair(
-      "townland", IDS_LIBADDRESSINPUT_TOWNLAND));
-  message_ids.insert(std::make_pair(
-      "village_township", IDS_LIBADDRESSINPUT_VILLAGE_TOWNSHIP));
-  return message_ids;
-}
+const NameIdMap kAdminAreaMessageIds = {
+  kAdminAreaInfoArray,
+  arraysize(kAdminAreaInfoArray)
+};
 
-const NameMessageIdMap& GetSublocalityMessageIds() {
-  static const NameMessageIdMap kSublocalityMessageIds(
-      InitSublocalityMessageIds());
-  return kSublocalityMessageIds;
-}
+const NameIdInfo kPostalCodeInfoArray[] = {
+  {"eircode", IDS_LIBADDRESSINPUT_EIR_CODE_LABEL},
+  {"pin", IDS_LIBADDRESSINPUT_PIN_CODE_LABEL},
+  {"postal", IDS_LIBADDRESSINPUT_POSTAL_CODE_LABEL},
+  {"zip", IDS_LIBADDRESSINPUT_ZIP_CODE_LABEL},
+};
 
-int GetMessageIdFromName(const std::string& name,
-                         const NameMessageIdMap& message_ids) {
-  NameMessageIdMap::const_iterator it = message_ids.find(name);
-  return it != message_ids.end() ? it->second : INVALID_MESSAGE_ID;
-}
+const NameIdMap kPostalCodeMessageIds = {
+  kPostalCodeInfoArray,
+  arraysize(kPostalCodeInfoArray),
+};
+
+const NameIdInfo kLocalityInfoArray[] = {
+  {"city", IDS_LIBADDRESSINPUT_LOCALITY_LABEL},
+  {"district", IDS_LIBADDRESSINPUT_DISTRICT},
+  {"post_town", IDS_LIBADDRESSINPUT_POST_TOWN},
+  {"suburb", IDS_LIBADDRESSINPUT_SUBURB},
+};
+
+const NameIdMap kLocalityMessageIds = {
+  kLocalityInfoArray,
+  arraysize(kLocalityInfoArray),
+};
+
+const NameIdInfo kSublocalityInfoArray[] = {
+  {"district", IDS_LIBADDRESSINPUT_DISTRICT},
+  {"neighborhood", IDS_LIBADDRESSINPUT_NEIGHBORHOOD},
+  {"suburb", IDS_LIBADDRESSINPUT_SUBURB},
+  {"townland", IDS_LIBADDRESSINPUT_TOWNLAND},
+  {"village_township", IDS_LIBADDRESSINPUT_VILLAGE_TOWNSHIP},
+};
+
+const NameIdMap kSublocalityMessageIds = {
+  kSublocalityInfoArray,
+  arraysize(kSublocalityInfoArray),
+};
+
+#ifndef _NDEBUG
+// Helper type used to check that all maps are sorted at runtime.
+// Should be used as a local static variable to ensure this is checked only
+// once per process. Usage is simply:
+//
+//  ... someFunction(....) {
+//      static StaticMapChecker map_checker;
+//      ... anything else ...
+//      }
+struct StaticMapChecker {
+  StaticMapChecker() {
+    assert(kAdminAreaMessageIds.IsSorted());
+    assert(kPostalCodeMessageIds.IsSorted());
+    assert(kLocalityMessageIds.IsSorted());
+    assert(kSublocalityMessageIds.IsSorted());
+  }
+};
+#endif  // _NDEBUG
 
 // Determines whether a given string is a reg-exp or a string. We consider a
 // string to be anything that doesn't contain characters with special meanings
@@ -217,6 +230,11 @@
 }
 
 void Rule::ParseJsonRule(const Json& json) {
+#ifndef _NDEBUG
+  // Don't remove, see StaticMapChecker comments above.
+  static StaticMapChecker map_checker;
+  #endif  // !_NDEBUG
+
   std::string value;
   if (json.GetStringValueForKey("id", &value)) {
     id_.swap(value);
@@ -272,23 +290,19 @@
   }
 
   if (json.GetStringValueForKey("state_name_type", &value)) {
-    admin_area_name_message_id_ =
-        GetMessageIdFromName(value, GetAdminAreaMessageIds());
+    admin_area_name_message_id_ = kAdminAreaMessageIds.GetIdFromName(value);
   }
 
   if (json.GetStringValueForKey("zip_name_type", &value)) {
-    postal_code_name_message_id_ =
-        GetMessageIdFromName(value, GetPostalCodeMessageIds());
+    postal_code_name_message_id_ = kPostalCodeMessageIds.GetIdFromName(value);
   }
 
   if (json.GetStringValueForKey("locality_name_type", &value)) {
-    locality_name_message_id_ =
-        GetMessageIdFromName(value, GetLocalityMessageIds());
+    locality_name_message_id_ = kLocalityMessageIds.GetIdFromName(value);
   }
 
   if (json.GetStringValueForKey("sublocality_name_type", &value)) {
-    sublocality_name_message_id_ =
-        GetMessageIdFromName(value, GetSublocalityMessageIds());
+    sublocality_name_message_id_ = kSublocalityMessageIds.GetIdFromName(value);
   }
 
   if (json.GetStringValueForKey("name", &value)) {