Add ghost text autocomplete location search to homepage hero
Replaces community dropdown with text input supporting city/ZIP search: - Ghost text autocomplete shows inline suggestion as user types - Tab to accept, auto-fill on blur, Enter uses partial match - Geolocation button for "Use My Location" searches - AJAX endpoint returns MN/IA cities and zipcodes with 1-hour cache - MLS query now supports lat/lng/radius for distance-based filtering - Updated Census Bureau 2023 Gazetteer data (32,329 US cities) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Regular → Executable
+204
-16
@@ -50,6 +50,85 @@ class MLS_Query {
|
||||
return "(street_number IS NULL OR (street_number != 'TBD' AND street_number NOT LIKE 'TBD %'))";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coordinates for a zip code from geo table
|
||||
*
|
||||
* @param string $zipcode The zip code to look up
|
||||
* @return array|null Array with 'latitude' and 'longitude' or null if not found
|
||||
*/
|
||||
private function get_zipcode_coordinates($zipcode) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->geo_zipcodes_table();
|
||||
$result = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT latitude, longitude FROM {$table} WHERE zipcode = %s",
|
||||
$zipcode
|
||||
));
|
||||
|
||||
if ($result) {
|
||||
return array(
|
||||
'latitude' => (float) $result->latitude,
|
||||
'longitude' => (float) $result->longitude,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get coordinates for a city from geo table
|
||||
* Searches for city in MN and IA states (matching MLS_ALLOWED_STATES)
|
||||
*
|
||||
* @param string $city The city name to look up
|
||||
* @return array|null Array with 'latitude' and 'longitude' or null if not found
|
||||
*/
|
||||
private function get_city_coordinates($city) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->geo_cities_table();
|
||||
|
||||
// Search in allowed states (MN, IA)
|
||||
$allowed_states = defined('MLS_ALLOWED_STATES') ? MLS_ALLOWED_STATES : array('MN', 'IA');
|
||||
$placeholders = implode(',', array_fill(0, count($allowed_states), '%s'));
|
||||
|
||||
$values = array_merge(array($city), $allowed_states);
|
||||
|
||||
$result = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT latitude, longitude FROM {$table} WHERE city = %s AND state_code IN ({$placeholders}) LIMIT 1",
|
||||
$values
|
||||
));
|
||||
|
||||
if ($result) {
|
||||
return array(
|
||||
'latitude' => (float) $result->latitude,
|
||||
'longitude' => (float) $result->longitude,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SQL for distance-based filtering using Haversine formula
|
||||
* Returns properties within specified miles of a center point
|
||||
*
|
||||
* @param float $lat Center latitude
|
||||
* @param float $lng Center longitude
|
||||
* @param float $miles Radius in miles
|
||||
* @return string SQL expression for distance filter
|
||||
*/
|
||||
private function get_distance_filter_sql($lat, $lng, $miles) {
|
||||
// Haversine formula: distance in miles
|
||||
// 3959 is Earth's radius in miles
|
||||
return sprintf(
|
||||
"(3959 * acos(cos(radians(%f)) * cos(radians(latitude)) * cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) <= %f",
|
||||
$lat,
|
||||
$lng,
|
||||
$lat,
|
||||
$miles
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties matching criteria
|
||||
*
|
||||
@@ -65,6 +144,9 @@ class MLS_Query {
|
||||
'city' => null,
|
||||
'county' => null,
|
||||
'postal_code' => null,
|
||||
'center_lat' => null, // Latitude for radius search
|
||||
'center_lng' => null, // Longitude for radius search
|
||||
'radius' => 30, // Miles for radius search (default 30)
|
||||
'min_price' => null,
|
||||
'max_price' => null,
|
||||
'min_beds' => null,
|
||||
@@ -125,9 +207,50 @@ class MLS_Query {
|
||||
$values[] = $args['property_type'];
|
||||
}
|
||||
|
||||
// City and postal_code are mutually exclusive - city takes priority
|
||||
if ($args['city']) {
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
// Look up city coordinates for radius search
|
||||
$city_coords = $this->get_city_coordinates($args['city']);
|
||||
if ($city_coords) {
|
||||
// Match exact city OR within 15 miles of city center
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
$city_coords['latitude'],
|
||||
$city_coords['longitude'],
|
||||
15 // miles
|
||||
);
|
||||
$where[] = "(city = %s OR ({$distance_filter}))";
|
||||
$values[] = $args['city'];
|
||||
} else {
|
||||
// Fallback to exact match if city not in geo table
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
}
|
||||
} elseif ($args['postal_code']) {
|
||||
// Only apply postal_code filter if city is not set
|
||||
// Look up zip code coordinates for radius search
|
||||
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']);
|
||||
if ($zip_coords) {
|
||||
// Match exact zip code OR within 20 miles of zip code center
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
$zip_coords['latitude'],
|
||||
$zip_coords['longitude'],
|
||||
20 // miles
|
||||
);
|
||||
$where[] = "(postal_code = %s OR ({$distance_filter}))";
|
||||
$values[] = $args['postal_code'];
|
||||
} else {
|
||||
// Fallback to exact match if zip code not in geo table
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
}
|
||||
} elseif ($args['center_lat'] && $args['center_lng']) {
|
||||
// Direct lat/lng radius search (from homepage location search)
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
(float) $args['center_lat'],
|
||||
(float) $args['center_lng'],
|
||||
(int) $args['radius']
|
||||
);
|
||||
$where[] = "({$distance_filter})";
|
||||
}
|
||||
|
||||
if ($args['county']) {
|
||||
@@ -135,11 +258,6 @@ class MLS_Query {
|
||||
$values[] = $args['county'];
|
||||
}
|
||||
|
||||
if ($args['postal_code']) {
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
}
|
||||
|
||||
if ($args['min_price']) {
|
||||
$where[] = 'list_price >= %d';
|
||||
$values[] = (int) $args['min_price'];
|
||||
@@ -428,9 +546,51 @@ class MLS_Query {
|
||||
$values[] = $args['property_type'];
|
||||
}
|
||||
|
||||
// City and postal_code are mutually exclusive - city takes priority
|
||||
if (!empty($args['city'])) {
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
// Look up city coordinates for radius search
|
||||
$city_coords = $this->get_city_coordinates($args['city']);
|
||||
if ($city_coords) {
|
||||
// Match exact city OR within 15 miles of city center
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
$city_coords['latitude'],
|
||||
$city_coords['longitude'],
|
||||
15 // miles
|
||||
);
|
||||
$where[] = "(city = %s OR ({$distance_filter}))";
|
||||
$values[] = $args['city'];
|
||||
} else {
|
||||
// Fallback to exact match if city not in geo table
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
}
|
||||
} elseif (!empty($args['postal_code'])) {
|
||||
// Only apply postal_code filter if city is not set
|
||||
// Look up zip code coordinates for radius search
|
||||
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']);
|
||||
if ($zip_coords) {
|
||||
// Match exact zip code OR within 20 miles of zip code center
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
$zip_coords['latitude'],
|
||||
$zip_coords['longitude'],
|
||||
20 // miles
|
||||
);
|
||||
$where[] = "(postal_code = %s OR ({$distance_filter}))";
|
||||
$values[] = $args['postal_code'];
|
||||
} else {
|
||||
// Fallback to exact match if zip code not in geo table
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
}
|
||||
} elseif (!empty($args['center_lat']) && !empty($args['center_lng'])) {
|
||||
// Direct lat/lng radius search (from homepage location search)
|
||||
$radius = !empty($args['radius']) ? (int) $args['radius'] : 30;
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
(float) $args['center_lat'],
|
||||
(float) $args['center_lng'],
|
||||
$radius
|
||||
);
|
||||
$where[] = "({$distance_filter})";
|
||||
}
|
||||
|
||||
if (!empty($args['county'])) {
|
||||
@@ -644,14 +804,42 @@ class MLS_Query {
|
||||
$values[] = $args['property_type'];
|
||||
}
|
||||
|
||||
// City and postal_code are mutually exclusive - city takes priority
|
||||
if (!empty($args['city'])) {
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
}
|
||||
|
||||
if (!empty($args['postal_code'])) {
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
// Look up city coordinates for radius search
|
||||
$city_coords = $this->get_city_coordinates($args['city']);
|
||||
if ($city_coords) {
|
||||
// Match exact city OR within 15 miles of city center
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
$city_coords['latitude'],
|
||||
$city_coords['longitude'],
|
||||
15 // miles
|
||||
);
|
||||
$where[] = "(city = %s OR ({$distance_filter}))";
|
||||
$values[] = $args['city'];
|
||||
} else {
|
||||
// Fallback to exact match if city not in geo table
|
||||
$where[] = 'city = %s';
|
||||
$values[] = $args['city'];
|
||||
}
|
||||
} elseif (!empty($args['postal_code'])) {
|
||||
// Only apply postal_code filter if city is not set
|
||||
// Look up zip code coordinates for radius search
|
||||
$zip_coords = $this->get_zipcode_coordinates($args['postal_code']);
|
||||
if ($zip_coords) {
|
||||
// Match exact zip code OR within 20 miles of zip code center
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
$zip_coords['latitude'],
|
||||
$zip_coords['longitude'],
|
||||
20 // miles
|
||||
);
|
||||
$where[] = "(postal_code = %s OR ({$distance_filter}))";
|
||||
$values[] = $args['postal_code'];
|
||||
} else {
|
||||
// Fallback to exact match if zip code not in geo table
|
||||
$where[] = 'postal_code = %s';
|
||||
$values[] = $args['postal_code'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($args['min_price'])) {
|
||||
|
||||
Reference in New Issue
Block a user