¿Cómo puedo implementar una búsqueda basada en ubicación (código postal) en WordPress?
Estoy trabajando en un sitio de directorio de negocios locales que utilizará tipos de publicación personalizados para las entradas de negocios. Uno de los campos será "Código Postal". ¿Cómo puedo configurar una búsqueda basada en ubicación?
Me gustaría que los visitantes pudieran ingresar su código postal y elegir una categoría para mostrar todos los negocios dentro de un cierto radio, o todos los negocios ordenados por distancia. Vi algunos plugins que dicen hacer esto pero no son compatibles con WordPress 3.0. ¿Alguna sugerencia?

Modificaría la respuesta de gabrielk y la publicación de blog vinculada utilizando índices de bases de datos y minimizando el número de cálculos reales de distancia.
Si conoces las coordenadas del usuario y la distancia máxima (digamos 10km), puedes dibujar un cuadro delimitador de 20km por 20km con la ubicación actual en el centro. Obtén estas coordenadas delimitadoras y consulta solo las tiendas entre estas latitudes y longitudes. No uses aún funciones trigonométricas en tu consulta de base de datos, ya que esto evitará que se usen los índices. (Así que podrías obtener una tienda que esté a 12km de ti si está en la esquina noreste del cuadro delimitador, pero la descartaremos en el siguiente paso.)
Solo calcula la distancia (en línea recta o con direcciones reales de conducción, como prefieras) para las pocas tiendas que se devuelvan. Esto mejorará drásticamente el tiempo de procesamiento si tienes un gran número de tiendas.
Para la búsqueda relacionada ("dame las diez tiendas más cercanas") puedes hacer una búsqueda similar, pero con una suposición inicial de distancia (así que empiezas con un área de 10km por 10km, y si no tienes suficientes tiendas, la amplías a 20km por 20km y así sucesivamente). Para esta suposición inicial de distancia, calculas una vez el número de tiendas sobre el área total y lo usas. O registra el número de consultas necesarias y adáptalo con el tiempo.
He añadido un ejemplo de código completo en la pregunta relacionada de Mike, y aquí hay una extensión que te da las ubicaciones X más cercanas (rápido y apenas probado):
class Monkeyman_Geo_ClosestX extends Monkeyman_Geo
{
public static $closestXStartDistanceKm = 10;
public static $closestXMaxDistanceKm = 1000; // No buscar más allá de esto
public function addAdminPages()
{
parent::addAdminPages();
add_management_page( 'Prueba de ubicación más cercana', 'Prueba de ubicación más cercana', '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>Número de publicaciones: <input size="5" name="post_count" value="10"/></p>
<p>Latitud del centro: <input size="10" name="center_lat" value="{$default_lat}"/>
<br/>Longitud del centro: <input size="10" name="center_lon" value="{$default_lon}"/></p>
<p><input type="submit" name="search" value="¡Buscar!"/></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));
}
/**
* Obtiene las X publicaciones más cercanas a una ubicación dada
*
* Esto podría devolver más de X resultados, y nunca más de
* self::$closestXMaxDistanceKm de distancia (para evitar búsquedas infinitas)
* Los resultados están ordenados por distancia
*
* El algoritmo comienza con todas las ubicaciones no más lejos que
* self::$closestXStartDistanceKm, y luego expande esta área
* (duplicando la distancia) hasta encontrar suficientes coincidencias.
*
* El número de cálculos costosos debe minimizarse.
*/
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) {
// Solo incluir aquellas que están dentro del radio del círculo, no del cuadro delimitador, de lo contrario podríamos perder algunas más cercanas en el siguiente paso
$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();

Primero necesitas una tabla que se vea algo así:
zip_code lat lon
10001 40.77 73.98
...poblada para cada código postal. Puedes ampliar esto agregando campos de ciudad y estado si deseas buscar de esa manera.
Luego, a cada tienda se le puede asignar un código postal, y cuando necesites calcular la distancia puedes unir la tabla de lat/long con los datos de la tienda.
Después consultarás esa tabla para obtener la latitud y longitud de los códigos postales de la tienda y del usuario. Una vez que los obtengas, puedes llenar tu arreglo y pasarlo a una función "obtener distancia":
$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 'La tienda ' . $id . ' está a ' . $distance . ' de distancia';
}
}
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);
}
Esto está pensado como una prueba de concepto, no como código que realmente recomendaría implementar. Si tienes 10,000 tiendas, por ejemplo, sería una operación bastante costosa consultarlas todas, recorrerlas y ordenarlas en cada solicitud.

¿Se podrían almacenar en caché los resultados? Además, ¿sería más fácil consultar una de las bases de datos de códigos postales disponibles comercialmente (o gratuitas si hay alguna)?

@matt - ¿Qué quieres decir con consultar una de las bases disponibles comercialmente o gratuitas? El almacenamiento en caché siempre debería ser posible, consulta http://codex.wordpress.org/Transients_API

@hakre: No importa, creo que estoy hablando más de lo que sé. Me refiero a usar bases de datos de códigos postales (USPS, Google Maps...) para obtener las distancias pero no me di cuenta de que probablemente no almacenan las distancias, solo almacenan el código postal y las coordenadas y sería mi responsabilidad calcularlas.

La documentación de MySQL también incluye información sobre extensiones espaciales. Curiosamente, la función estándar distance() no está disponible, pero revisa esta página: http://dev.mysql.com/tech-resources/articles/4.1/gis-with-mysql.html para obtener detalles sobre cómo "convertir los dos valores POINT a un LINESTRING y luego calcular la longitud de este."
Ten en cuenta que cada proveedor probablemente ofrecerá diferentes latitudes y longitudes que representan el "centroide" de un código postal. También vale la pena saber que no existen archivos reales definidos de "límites" de códigos postales. Cada proveedor tendrá su propio conjunto de límites que coinciden aproximadamente con las listas específicas de direcciones que conforman un código postal del USPS. (Por ejemplo, en algunos "límites" necesitarías incluir ambos lados de la calle, en otros, solo uno.) Las Áreas de Tabulación de Códigos Postales (ZCTAs), ampliamente utilizadas por los proveedores, "no representan con precisión las áreas de entrega de códigos postales, y no incluyen todos los códigos postales utilizados para el envío de correo" http://www.census.gov/geo/www/cob/zt_metadata.html
Muchos negocios del centro tendrán su propio código postal. Desearás un conjunto de datos lo más completo posible, así que asegúrate de encontrar una lista de códigos postales que incluya tanto códigos postales de "punto" (generalmente negocios), como códigos postales de "límite".
Tengo experiencia trabajando con los datos de códigos postales de http://www.semaphorecorp.com/. Incluso esos no eran 100% precisos. Por ejemplo, cuando mi campus adoptó una nueva dirección postal y un nuevo código postal, el código postal fue mal ubicado. Dicho esto, era la única fuente de datos que encontré que incluso TENÍA el nuevo código postal, tan poco después de su creación.
Tenía una receta en mi libro sobre exactamente cómo cumplir con tu solicitud... en Drupal. Se basaba en el módulo Google Maps Tools ( http://drupal.org/project/gmaps, que no debe confundirse con http://drupal.org/project/gmap, también un módulo valioso). Podrías encontrar algún código de muestra útil en esos módulos, aunque, por supuesto, no funcionarán directamente en WordPress.
