Как реализовать поиск по местоположению (почтовому индексу) в WordPress?

28 сент. 2010 г., 01:32:13
Просмотры: 26.2K
Голосов: 21

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

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

9
Комментарии

Я начинаю награду, потому что это интересный и сложный вопрос. У меня есть несколько собственных идей... но я хочу посмотреть, сможет ли кто-то предложить что-то более элегантное (и более простое в реализации).

EAMann EAMann
3 окт. 2010 г. 17:53:59

Спасибо, EAMann. Это может помочь: http://briancray.com/2009/04/01/how-to-calculate-the-distance-between-two-addresses-with-javascript-and-google-maps-api/

matt matt
3 окт. 2010 г. 19:40:39

Единственное предложение, которое у меня есть на данный момент — использовать плагин, например pods

NetConstructor.com NetConstructor.com
4 окт. 2010 г. 11:19:44

@NetConstructor.com - Я бы не рекомендовал использовать Pods для этого; на самом деле он не предоставляет никаких преимуществ по сравнению с пользовательскими типами записей для решения данной проблемы.

MikeSchinkel MikeSchinkel
5 окт. 2010 г. 02:36:59

@matt: Недавно я реализовал нечто очень похожее, хотя сайт еще не завершен и не развернут. На самом деле, там довольно много деталей. Я планирую упаковать это в виде плагина для поиска магазинов, но пока не могу выложить это как универсальное решение. Свяжись со мной в личке, и, возможно, я смогу помочь, если не найдешь нужного ответа.

MikeSchinkel MikeSchinkel
5 окт. 2010 г. 02:38:13

@mikeschinkel -- Привет, Майк, единственная причина, по которой я упомянул Pods, заключается в том, что он может дать Крису больше гибкости, так как, насколько я знаю, он создает собственные таблицы в базе данных, что может упростить процесс или сделать его более надежным. Но, думаю, ты прав, то же самое можно сделать через пользовательский тип записи. Кстати, Майк... жди моего письма в ближайшее время ;-)

NetConstructor.com NetConstructor.com
5 окт. 2010 г. 10:43:45

@NetConstructor: Я рассматривал этот подход, используя руководство до полудня в понедельник, но беспокоился, что он не будет таким масштабируемым или перспективным, как использование нативной функциональности.

matt matt
5 окт. 2010 г. 20:11:12

@mikeschinkel: Спасибо, полагаю, ты получишь от меня email. Просто интересно, используешь ли ты какую-либо стороннюю базу данных?

matt matt
5 окт. 2010 г. 20:12:07

@matt: Нет, написал всё сам. Система делает запросы к Google Maps и кеширует результаты для будущего использования, а также чтобы минимизировать количество ежедневных обращений к Google Maps. Также кешируются расстояния, чтобы их можно было индексировать, а не выполнять неиндексируемый запрос. Логика кеширования — это отчасти то, откуда берётся сложность.

MikeSchinkel MikeSchinkel
6 окт. 2010 г. 00:33:06
Показать остальные 4 комментариев
Все ответы на вопрос 3
0
10

Я бы модифицировал ответ от 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();
7 окт. 2010 г. 12:52:40
3

Сначала вам понадобится таблица, которая выглядит примерно так:

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 магазинов, запрос всех данных, их перебор и сортировка при каждом запросе будут довольно ресурсоёмкой операцией.

5 окт. 2010 г. 01:04:21
Комментарии

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

matt matt
5 окт. 2010 г. 20:03:08

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

hakre hakre
6 окт. 2010 г. 13:08:49

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

matt matt
7 окт. 2010 г. 23:32:05
0

Документация 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.

7 окт. 2010 г. 23:27:00