I'm working on a website that required an easy way to associate locations with a content type. It should be possible to first select a Province from a dropdown list, and then choose from a list of the towns in that province.

I have implemented this using the following popular modules:

  • Hierarchical Select: Provides easy to use drill-down select boxes, exactly what the project needed.
  • Location: Associates location information with entities, including taxonomy terms. As a bonus it contains a list of Belgian towns and post codes.
  • References: Allows to add taxonomy reference fields to content types.

Setting it up was easy enough: I created a vocabulary with Locative Information enabled and a content type containing a reference field to this vocabulary and chose the Hierarchical Select widget for this field.

Now all I needed was to populate the vocabulary so it contains all Belgian towns, with the province as parent term. I imported the Belgian zipcodes database that is included in the Location module, and made a script that uses this data to populate the vocabulary.

If you have the same use case, you'll need to adapt the script so it matches your country's provinces / states, but I hope this will save you some time.

<?php
/**
 * @file
 * Import location information into a taxonomy vocabulary.
 *
 * This script takes the Belgian 'zipcodes' table as supplied by the Location
 * module and uses it to populate a taxonomy vocabulary. The Belgian provinces
 * are used as parent items with the towns as children. Each town will get its
 * location information associated with it.
 *
 * This requires a taxonomy vocabulary to be set up with Location support. If
 * there are already terms present in the vocabulary they will be deleted
 * without warning.
 *
 * Usage:
 * - Enter the vocabulary id of the vocabulary you want to populate below and
 *   save the script in the Drupal root folder, or inside the 'scripts' folder.
 * - Run the script with drush:
 *   drush scr location_import.php
 */
// The ID of the vocabulary that will be populated with location information. 
define('LOCATION_VID', 1);

// Check if required modules are present.
if (!module_exists('location') || !module_exists('taxonomy') || !module_exists('location_taxonomy')) {
  drush_log('This script requires the following modules: Taxonomy, Location, Location Taxonomy.', 'error');
  exit;
}

// Check if vocabulary exists and has Locative Information enabled.
$settings = variable_get('location_taxonomy_' . LOCATION_VID, FALSE);
if (!isset($settings['multiple']['max']) || !$settings['multiple']['max']) {
  drush_log('Make sure the vocabulary has Locative Information enabled: set the "Maximum number of locations" to "1".', 'error');
  exit;
}

// A list of Belgian provinces mapped to their state codes.
$provinces = array(
  'BRU' => array('name' => 'Brussel'),
  'VAN' => array('name' => 'Antwerpen'),
  'VBR' => array('name' => 'Vlaams-Brabant'),
  'VLI' => array('name' => 'Limburg'),
  'VOV' => array('name' => 'Oost-Vlaanderen'),
  'VWV' => array('name' => 'West-Vlaanderen'),
  'WBR' => array('name' => 'Brabant Wallon'),
  'WHT' => array('name' => 'Hainaut'),
  'WLG' => array('name' => 'Li├Ęge'),
  'WLX' => array('name' => 'Luxembourg'),
  'WNA' => array('name' => 'Namur'),
);

// Delete all entries in the vocabulary.
$terms = taxonomy_get_tree(LOCATION_VID);
foreach ($terms as $term) {
  drush_print('Deleting term ' . $term->name);
  taxonomy_term_delete($term->tid);
}

// Create terms for the provinces.
foreach ($provinces as $key => $province) {
  drush_print('Creating parent term ' . $province['name']);
  $term = new stdClass();
  $term->vid = LOCATION_VID;
  $term->name = $province['name'];
  taxonomy_term_save($term);
  // Retain the tids of the newly created terms so they can be referenced when
  // saving the towns.
  $provinces[$key]['tid'] = $term->tid;
}

// Fetch all location information.
$query = db_select('zipcodes', 'zc');
$query->addField('zc', 'zip', 'postal_code');
$query->addField('zc', 'city');
$query->addField('zc', 'state', 'province');
$query->addField('zc', 'country');
$query->addField('zc', 'latitude');
$query->addField('zc', 'longitude');
$query->orderBy('postal_code', 'ASC');
$locations = $query->execute()->fetchAll(PDO::FETCH_ASSOC);

// Populate the terms and locations.
foreach ($locations as $location) {
  // The city names might contain accented characters. 
  $location['city'] = utf8_decode($location['city']);

  drush_print($location['postal_code'] . ' ' . $location['city']);

  // Create a new taxonomy term with the province as parent.
  $term = new stdClass();
  $term->vid = LOCATION_VID;
  $term->name = $location['city'];
  $term->parent = $provinces[$location['province']]['tid'];
  taxonomy_term_save($term);

  // Add the location to the database. location_save_locations() expects this to
  // be wrapped in an array.
  $location = array($location);
  location_save_locations($location, array('genid' => 'taxonomy:' . $term->tid));
}

drush_log('Locations imported successfully.', 'success');