Autocomplete By Zip Code

The admin of a Drupal site enters and updates many addresses every day. For every contact and organization account on their site, they have a city, state, zip code, county, and region. Really, the address fields are just redundant information. Everything can be gleaned from the zip code. So, in an effort to become more efficient and reduce data entry errors, the client requested that the city, state, county, and region fields autocomplete by entering the zip code.

After extensive googling, it was clear there was no established way to accomplish this. So here's what I came up with:

Step 1

I downloaded a free zip code database from somewhere (no idea where anymore. Geonames?) that included city, state, zip, and county. And then I added a column containing the client-defined regions and imported the table into the site database.

Step 2

Using hook_form_form_id_alter in MYMODULE.module, add an #autocomplete_path property to the zip code field. My zip code field is part of the Address Field module, as are my city and state fields. My county and region fields are taxonomy fields.

 * Implements hook_form_form_id_alter().
function MYMODULE_form_redhen_contact_contact_form_alter (&$form, &$form_state) {
    $form['field_emp_address'][LANGUAGE_NONE][0]['locality_block']['postal_code']['#autocomplete_path'] = 'zipcode/autocomplete';

Step 3

Also in the MYMODULE.module file, add a menu item with a callback. The access arguments may need to be changed based on your situation. The one I am using would likely not be available on most sites so I changed it to 'administer nodes'.

 * Implements hook_menu().
function MYMODULE_menu() {
  $items = array();

  // Zipcode lookup
  $items['zipcode/autocomplete'] = array(
    'title' => 'zipcode autocomplete',
    'page callback' => 'zipcode_autocomplete',
    'access callback' => 'user_access',
    'access arguments' => array('administer nodes'),
    'type' => MENU_CALLBACK,
  return $items;

Step 4

The callback function in MYMODULE.module. The function queries the database for the zip code the admin entered into the zip code field and encodes the results using json.

 * Get zip code matches from the database.
function zipcode_autocomplete($string = '') {
  $matches = array();
  if ($string) {
    // Find top 10 matches from the database based on the number of digits entered
    $result = db_select('zipcode')
      ->fields('zipcode', array('zip', 'city', 'state_abbr', 'county', 'region'))
      ->condition('zip', db_like($string) . '%', 'LIKE')
      ->range(0, 10)->execute();

    // Add the results to an array that gets displayed by a modified autocomplete.js
    foreach ($result as $zipcode) {
      $matches[$zipcode->zip]['city'] = $zipcode->city;
      $matches[$zipcode->zip]['state'] = $zipcode->state_abbr;
      $matches[$zipcode->zip]['zip'] = $zipcode->zip;
      $matches[$zipcode->zip]['county'] = $zipcode->county;
      $matches[$zipcode->zip]['region'] = $zipcode->region;
      // This is what actually gets shown to the user as the list of possible matches
      $matches[$zipcode->zip]['label'] = check_plain($zipcode->zip . ' - ' . $zipcode->city . ', ' . $zipcode->state_abbr);

Step 5

Copy the automcomplete.js file from the misc folder and place it into MYMODULE folder. (Yeah, this feels hacky but it's the least hacky-ish way I found to accomplish this functionality.) Attach the file to the form where it will be used by adding the following line to the hook_form_form_id alter from Step 2. This should override the misc/autocomplete.js file by appearing later in the head of the page.

$form['#attached']['js'] = array(drupal_get_path('module', MYMODULE') . '/autocomplete.js');

Step 6

Make the changes to autocomplete.js. Here is a patch of the changes I needed to make for it to work for the zip code autocomplete and still work for any other autocomplete fields on the same form.