Как реализовать поиск по местоположению (почтовому индексу) в WordPress?
Я работаю над сайтом локального бизнес-каталога, который будет использовать произвольные типы записей (custom post types) для бизнес-записей. Одним из полей будет "Почтовый индекс". Как можно настроить поиск по местоположению?
Я хотел бы, чтобы посетители могли ввести свой почтовый индекс и выбрать категорию, после чего увидеть все компании в определенном радиусе или все компании, отсортированные по расстоянию. Я видел несколько плагинов, которые заявляют о такой функциональности, но они не поддерживают WordPress 3.0. Есть ли какие-либо предложения?

Я бы модифицировал ответ от gabrielk и связанный пост в блоге, используя индексы базы данных и минимизацию количества реальных расчетов расстояний.
Если вам известны координаты пользователя и максимальное расстояние (например, 10 км), вы можете нарисовать ограничивающий прямоугольник размером 20 км на 20 км с текущим местоположением в центре. Получите эти граничные координаты и запрашивайте только магазины между этими широтами и долготами. Пока не используйте тригонометрические функции в запросе к базе данных, так как это предотвратит использование индексов. (Таким образом, вы можете получить магазин, который находится в 12 км от вас, если он находится в северо-восточном углу ограничивающего прямоугольника, но мы отфильтруем его на следующем шаге.)
Вычисляйте расстояние (по прямой или с учетом реальных маршрутов, как вам удобно) только для нескольких возвращенных магазинов. Это значительно улучшит время обработки, если у вас большое количество магазинов.
Для связанного поиска ("показать десять ближайших магазинов") вы можете выполнить аналогичный поиск, но с начальным предположением о расстоянии (вы начинаете с области 10 км на 10 км, и если у вас недостаточно магазинов, расширяете ее до 20 км на 20 км и так далее). Для этого начального предположения вы один раз вычисляете количество магазинов по всей области и используете это значение. Или записываете количество необходимых запросов и адаптируетесь со временем.
Я добавил полный пример кода в связанном вопросе Mike, а вот расширение, которое дает вам ближайшие X местоположений (быстрое и слабо протестированное):
class Monkeyman_Geo_ClosestX extends Monkeyman_Geo
{
public static $closestXStartDistanceKm = 10;
public static $closestXMaxDistanceKm = 1000; // Не искать за пределами этого расстояния
public function addAdminPages()
{
parent::addAdminPages();
add_management_page( 'Тест ближайших местоположений', 'Тест ближайших местоположений', 'edit_posts', __FILE__ . 'closesttest', array(&$this, 'doClosestTestPage'));
}
public function doClosestTestPage()
{
if (!array_key_exists('search', $_REQUEST)) {
$default_lat = ini_get('date.default_latitude');
$default_lon = ini_get('date.default_longitude');
echo <<<EOF
<form action="" method="post">
<p>Количество записей: <input size="5" name="post_count" value="10"/></p>
<p>Центральная широта: <input size="10" name="center_lat" value="{$default_lat}"/>
<br/>Центральная долгота: <input size="10" name="center_lon" value="{$default_lon}"/></p>
<p><input type="submit" name="search" value="Поиск!"/></p>
</form>
EOF;
return;
}
$post_count = intval($_REQUEST['post_count']);
$center_lon = floatval($_REQUEST['center_lon']);
$center_lat = floatval($_REQUEST['center_lat']);
var_dump(self::getClosestXPosts($center_lon, $center_lat, $post_count));
}
/**
* Получить ближайшие X записей к заданному местоположению
*
* Может вернуть больше чем X результатов, но никогда не более чем
* self::$closestXMaxDistanceKm (чтобы предотвратить бесконечный поиск)
* Результаты сортируются по расстоянию
*
* Алгоритм начинается со всех местоположений не далее чем
* self::$closestXStartDistanceKm, затем расширяет эту область
* (удваивая расстояние) пока не будет найдено достаточно совпадений.
*
* Количество ресурсоемких вычислений минимизировано.
*/
public static function getClosestXPosts($center_lon, $center_lat, $post_count)
{
$search_distance = self::$closestXStartDistanceKm;
$close_posts = array();
while (count($close_posts) < $post_count && $search_distance < self::$closestXMaxDistanceKm) {
list($north_lat, $east_lon, $south_lat, $west_lon) = self::getBoundingBox($center_lat, $center_lon, $search_distance);
$geo_posts = self::getPostsInBoundingBox($north_lat, $east_lon, $south_lat, $west_lon);
foreach ($geo_posts as $geo_post) {
if (array_key_exists($geo_post->post_id, $close_posts)) {
continue;
}
$post_lat = floatval($geo_post->lat);
$post_lon = floatval($geo_post->lon);
$post_distance = self::calculateDistanceKm($center_lat, $center_lon, $post_lat, $post_lon);
if ($post_distance < $search_distance) {
// Включаем только те, что в радиусе круга, а не ограничивающего прямоугольника, иначе можем пропустить более близкие на следующем шаге
$close_posts[$geo_post->post_id] = $post_distance;
}
}
$search_distance *= 2;
}
asort($close_posts);
return $close_posts;
}
}
$monkeyman_Geo_ClosestX_instace = new Monkeyman_Geo_ClosestX();

Сначала вам понадобится таблица, которая выглядит примерно так:
zip_code lat lon
10001 40.77 73.98
...заполненная для каждого почтового индекса. Вы можете расширить её, добавив поля города и штата, если хотите выполнять поиск и таким способом.
Затем каждому магазину можно присвоить почтовый индекс, и при необходимости расчёта расстояния вы сможете соединить таблицу с координатами (широта/долгота) с данными о магазинах.
После этого вы выполните запрос к этой таблице, чтобы получить широту и долготу для почтовых индексов магазина и пользователя. Получив эти данные, вы сможете заполнить массив и передать его в функцию "get distance":
$user_location = array(
'latitude' => 42.75,
'longitude' => 73.80,
);
$output = array();
$results = $wpdb->get_results("SELECT id, zip_code, lat, lon FROM store_table");
foreach ( $results as $store ) {
$store_location = array(
'zip_code' => $store->zip_code, // 10001
'latitude' => $store->lat, // 40.77
'longitude' => $store->lon, // 73.98
);
$distance = get_distance($store_location, $user_location, 'miles');
$output[$distance][$store->id] = $store_location;
}
ksort($output);
foreach ($output as $distance => $store) {
foreach ( $store as $id => $location ) {
echo 'Магазин ' . $id . ' находится на расстоянии ' . $distance . ' миль';
}
}
function get_distance($store_location, $user_location, $units = 'miles') {
if ( $store_location['longitude'] == $user_location['longitude'] &&
$store_location['latitude'] == $user_location['latitude']){
return 0;
$theta = ($store_location['longitude'] - $user_location['longitude']);
$distance = sin(deg2rad($store_location['latitude'])) * sin(deg2rad($user_location['latitude'])) + cos(deg2rad($store_location['latitude'])) * cos(deg2rad($user_location['latitude'])) * cos(deg2rad($theta));
$distance = acos($distance);
$distance = rad2deg($distance);
$distance = $distance * 60 * 1.1515;
if ( 'kilometers' == $units ) {
$distance = $distance * 1.609344;
}
return round($distance);
}
Это представлено как концептуальный пример, а не код, который я действительно рекомендовал бы реализовывать. Например, если у вас 10 000 магазинов, запрос всех данных, их перебор и сортировка при каждом запросе будут довольно ресурсоёмкой операцией.

Можно ли кэшировать результаты? Также, может быть проще запросить одну из коммерчески доступных (или бесплатных, если такие есть) баз данных почтовых индексов?

@matt - Что ты имеешь в виду под запросом коммерчески или бесплатно доступных баз? Кэширование всегда должно быть возможно, см. http://codex.wordpress.org/Transients_API

@hakre: Неважно, кажется, я сейчас говорю о вещах, в которых не разбираюсь. Я имел в виду использование баз данных почтовых индексов (USPS, Google Maps...) для получения расстояний, но не осознавал, что они, вероятно, не хранят расстояния, а только почтовые индексы и координаты, и мне самому придется их вычислять.

Документация MySQL также включает информацию о пространственных расширениях. Как ни странно, стандартная функция distance() недоступна, но на этой странице: http://dev.mysql.com/tech-resources/articles/4.1/gis-with-mysql.html вы найдёте подробности о том, как "преобразовать два значения POINT в LINESTRING, а затем вычислить длину получившейся линии".
Обратите внимание, что у каждого поставщика данных, скорее всего, будут разные значения широты и долготы, представляющие "центроид" почтового индекса. Также стоит знать, что не существует реально определённых файлов "границ" почтовых индексов. Каждый поставщик будет иметь свой набор границ, которые примерно соответствуют конкретным спискам адресов, составляющих почтовый индекс USPS. (Например, в некоторых "границах" вам нужно будет включить обе стороны улицы, в других — только одну.) Зоны табуляции почтовых индексов (ZCTAs), широко используемые поставщиками, "не точно изображают зоны доставки почтовых индексов и не включают все почтовые индексы, используемые для доставки почты" http://www.census.gov/geo/www/cob/zt_metadata.html
Многие предприятия в центре города будут иметь собственный почтовый индекс. Вам понадобится максимально полный набор данных, поэтому убедитесь, что вы нашли список почтовых индексов, включающий как "точечные" индексы (обычно предприятия), так и "граничные" почтовые индексы.
У меня есть опыт работы с данными почтовых индексов от http://www.semaphorecorp.com/. Даже они не были на 100% точными. Например, когда мой кампус получил новый почтовый адрес и новый индекс, этот индекс был ошибочно размещён. Тем не менее, это был единственный источник данных, который вообще содержал новый почтовый индекс вскоре после его создания.
В моей книге был рецепт о том, как именно выполнить ваш запрос... в Drupal. Он опирался на модуль Google Maps Tools (http://drupal.org/project/gmaps, не путать с http://drupal.org/project/gmap, также достойным модулем). Возможно, вы найдёте полезные примеры кода в этих модулях, хотя, конечно, они не будут работать "из коробки" в WordPress.
