This commit is contained in:
Hanson.xyz Dev
2026-01-04 17:50:08 -06:00
parent 7e45ce0756
commit acc8ac87a0
4131 changed files with 232562 additions and 250244 deletions
View File
View File
+151
View File
@@ -39,6 +39,7 @@ class MLS_CLI {
WP_CLI::add_command('mls cache', array($instance, 'cache'));
WP_CLI::add_command('mls recovery', array($instance, 'recovery'));
WP_CLI::add_command('mls media', array($instance, 'media'));
WP_CLI::add_command('mls geo', array($instance, 'geo'));
}
/**
@@ -1078,4 +1079,154 @@ class MLS_CLI {
rmdir($dir);
}
/**
* Geographic coordinate validation commands.
*
* ## OPTIONS
*
* <action>
* : Action to perform: validate, stats, or show-invalid
*
* ## EXAMPLES
*
* # Validate all property coordinates
* wp mls geo validate
*
* # Show validation statistics
* wp mls geo stats
*
* # Show properties with invalid coordinates
* wp mls geo show-invalid
*
* @param array $args Positional arguments
* @param array $assoc_args Associative arguments
*/
public function geo($args, $assoc_args) {
if (empty($args[0])) {
WP_CLI::error('Please specify an action: validate, stats, or show-invalid');
return;
}
$action = $args[0];
switch ($action) {
case 'validate':
$this->geo_validate();
break;
case 'stats':
$this->geo_stats();
break;
case 'show-invalid':
$this->geo_show_invalid();
break;
default:
WP_CLI::error("Unknown action: {$action}. Use validate, stats, or show-invalid.");
}
}
/**
* Validate all property coordinates
*/
private function geo_validate() {
WP_CLI::log('Validating property coordinates against state boundaries...');
WP_CLI::log('');
$progress = null;
$progress_callback = function($processed, $total, $valid, $invalid) use (&$progress) {
if ($progress === null) {
$progress = WP_CLI\Utils\make_progress_bar('Validating', $total);
}
$progress->tick();
};
$results = MLS_Geo_Validator::validate_all_properties($progress_callback);
if ($progress) {
$progress->finish();
}
WP_CLI::log('');
WP_CLI::success("Validation complete!");
WP_CLI::log(" Total properties: " . number_format($results['total']));
WP_CLI::log(" Valid coordinates: " . number_format($results['valid']));
WP_CLI::log(" Invalid coordinates: " . number_format($results['invalid']));
if ($results['invalid'] > 0) {
WP_CLI::log('');
WP_CLI::log("Properties with invalid coordinates will be excluded from map views.");
WP_CLI::log("Run 'wp mls geo show-invalid' to see details.");
}
}
/**
* Show coordinate validation statistics
*/
private function geo_stats() {
$stats = MLS_Geo_Validator::get_stats();
WP_CLI::log('Coordinate Validation Statistics:');
WP_CLI::log('');
WP_CLI::log(" Total properties: " . number_format($stats['total']));
WP_CLI::log(" Valid coordinates: " . number_format($stats['valid']));
WP_CLI::log(" Invalid coordinates: " . number_format($stats['invalid']));
WP_CLI::log(" Null coordinates: " . number_format($stats['null_coords']));
if ($stats['total'] > 0) {
$pct_valid = round(($stats['valid'] / $stats['total']) * 100, 1);
$pct_invalid = round(($stats['invalid'] / $stats['total']) * 100, 1);
WP_CLI::log('');
WP_CLI::log(" Valid %: {$pct_valid}%");
WP_CLI::log(" Invalid %: {$pct_invalid}%");
}
}
/**
* Show properties with invalid coordinates
*/
private function geo_show_invalid() {
global $wpdb;
$table = $this->plugin->get_db()->properties_table();
$invalid = $wpdb->get_results(
"SELECT listing_id, listing_key, street_number, street_name, city, state_or_province, latitude, longitude
FROM {$table}
WHERE coordinates_invalid = 1
ORDER BY city, street_name
LIMIT 100"
);
if (empty($invalid)) {
WP_CLI::success("No properties with invalid coordinates found.");
return;
}
WP_CLI::log("Properties with invalid coordinates (showing up to 100):");
WP_CLI::log('');
$items = array();
foreach ($invalid as $row) {
$address = trim($row->street_number . ' ' . $row->street_name);
$items[] = array(
'MLS ID' => $row->listing_id,
'Address' => $address ?: '(no address)',
'City' => $row->city,
'State' => $row->state_or_province,
'Lat' => $row->latitude,
'Lng' => $row->longitude,
);
}
WP_CLI\Utils\format_items('table', $items, array('MLS ID', 'Address', 'City', 'State', 'Lat', 'Lng'));
$total = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE coordinates_invalid = 1");
if ($total > 100) {
WP_CLI::log('');
WP_CLI::log("... and " . ($total - 100) . " more.");
}
}
}
View File
+73
View File
@@ -0,0 +1,73 @@
<?php
/**
* US State Bounding Boxes
*
* Approximate rectangular bounds for each US state.
* Used to validate that property coordinates fall within their claimed state.
*
* Format: state_code => [min_lat, max_lat, min_lng, max_lng]
*
* Data sourced from US Census Bureau state boundaries.
* Bounds include a small buffer for edge cases.
*/
if (!defined('ABSPATH')) {
exit;
}
return array(
'AL' => array(30.14, 35.01, -88.47, -84.89), // Alabama
'AK' => array(51.21, 71.39, -179.15, 179.77), // Alaska (crosses date line)
'AZ' => array(31.33, 37.00, -114.82, -109.04), // Arizona
'AR' => array(33.00, 36.50, -94.62, -89.64), // Arkansas
'CA' => array(32.53, 42.01, -124.42, -114.13), // California
'CO' => array(36.99, 41.00, -109.06, -102.04), // Colorado
'CT' => array(40.95, 42.05, -73.73, -71.79), // Connecticut
'DE' => array(38.45, 39.84, -75.79, -75.05), // Delaware
'FL' => array(24.40, 31.00, -87.63, -80.03), // Florida
'GA' => array(30.36, 35.00, -85.61, -80.84), // Georgia
'HI' => array(18.91, 22.24, -160.25, -154.81), // Hawaii
'ID' => array(41.99, 49.00, -117.24, -111.04), // Idaho
'IL' => array(36.97, 42.51, -91.51, -87.02), // Illinois
'IN' => array(37.77, 41.76, -88.10, -84.78), // Indiana
'IA' => array(40.37, 43.50, -96.64, -90.14), // Iowa
'KS' => array(36.99, 40.00, -102.05, -94.59), // Kansas
'KY' => array(36.50, 39.15, -89.57, -81.96), // Kentucky
'LA' => array(28.93, 33.02, -94.04, -88.82), // Louisiana
'ME' => array(43.06, 47.46, -71.08, -66.95), // Maine
'MD' => array(37.91, 39.72, -79.49, -75.05), // Maryland
'MA' => array(41.24, 42.89, -73.51, -69.93), // Massachusetts
'MI' => array(41.70, 48.19, -90.42, -82.41), // Michigan
'MN' => array(43.50, 49.38, -97.24, -89.49), // Minnesota
'MS' => array(30.17, 35.00, -91.66, -88.10), // Mississippi
'MO' => array(35.99, 40.61, -95.77, -89.10), // Missouri
'MT' => array(44.36, 49.00, -116.05, -104.04), // Montana
'NE' => array(40.00, 43.00, -104.05, -95.31), // Nebraska
'NV' => array(35.00, 42.00, -120.01, -114.04), // Nevada
'NH' => array(42.70, 45.31, -72.56, -70.70), // New Hampshire
'NJ' => array(38.93, 41.36, -75.56, -73.89), // New Jersey
'NM' => array(31.33, 37.00, -109.05, -103.00), // New Mexico
'NY' => array(40.50, 45.02, -79.76, -71.86), // New York
'NC' => array(33.84, 36.59, -84.32, -75.46), // North Carolina
'ND' => array(45.94, 49.00, -104.05, -96.55), // North Dakota
'OH' => array(38.40, 42.33, -84.82, -80.52), // Ohio
'OK' => array(33.62, 37.00, -103.00, -94.43), // Oklahoma
'OR' => array(41.99, 46.29, -124.57, -116.46), // Oregon
'PA' => array(39.72, 42.27, -80.52, -74.69), // Pennsylvania
'RI' => array(41.15, 42.02, -71.86, -71.12), // Rhode Island
'SC' => array(32.03, 35.22, -83.35, -78.54), // South Carolina
'SD' => array(42.48, 45.95, -104.06, -96.44), // South Dakota
'TN' => array(34.98, 36.68, -90.31, -81.65), // Tennessee
'TX' => array(25.84, 36.50, -106.65, -93.51), // Texas
'UT' => array(36.99, 42.00, -114.05, -109.04), // Utah
'VT' => array(42.73, 45.02, -73.44, -71.46), // Vermont
'VA' => array(36.54, 39.47, -83.68, -75.24), // Virginia
'WA' => array(45.54, 49.00, -124.76, -116.92), // Washington
'WV' => array(37.20, 40.64, -82.64, -77.72), // West Virginia
'WI' => array(42.49, 47.08, -92.89, -86.25), // Wisconsin
'WY' => array(40.99, 45.01, -111.06, -104.05), // Wyoming
'DC' => array(38.79, 38.99, -77.12, -76.91), // District of Columbia
'PR' => array(17.88, 18.52, -67.95, -65.22), // Puerto Rico
'VI' => array(17.62, 18.42, -65.08, -64.56), // US Virgin Islands
'GU' => array(13.23, 13.65, 144.62, 144.96), // Guam
);
View File
Can't render this file because it is too large.
View File
View File
View File
View File
View File
@@ -249,7 +249,8 @@ class MLS_Cluster {
$table = $this->db->properties_table();
// Build WHERE clause
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
// Exclude properties with invalid coordinates from map display
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
$values = array();
// Add state filter (MN, IA only)
@@ -291,7 +292,7 @@ class MLS_Cluster {
$values[] = (int) $args['min_beds'];
}
// Add bounds filter if provided
// Add bounds filter (BETWEEN is efficient for rectangular viewport queries)
if ($args['bounds'] && is_array($args['bounds']) && count($args['bounds']) === 4) {
list($sw_lat, $sw_lng, $ne_lat, $ne_lng) = $args['bounds'];
$where[] = 'latitude BETWEEN %f AND %f';
@@ -571,7 +572,8 @@ class MLS_Cluster {
global $wpdb;
$table = $this->db->properties_table();
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
// Exclude properties with invalid coordinates
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
$values = array();
// Add state filter (MN, IA only)
+60 -2
View File
@@ -13,7 +13,7 @@ class MLS_DB {
* Schema version for index migrations
* Increment this when adding new indexes or columns
*/
const SCHEMA_VERSION = 3;
const SCHEMA_VERSION = 5;
/**
* Get table name with prefix
@@ -415,8 +415,66 @@ class MLS_DB {
update_option('mls_schema_version', 3);
}
// Migration to schema version 4: Add spatial POINT column and index
if ($current_schema < 4) {
$table_properties = $wpdb->prefix . MLS_TABLE_PROPERTIES;
// Check if location column exists
$column_exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'location'",
DB_NAME,
$table_properties
));
if (!$column_exists) {
// Add POINT column (nullable initially for population)
$wpdb->query("ALTER TABLE {$table_properties} ADD COLUMN location POINT SRID 4326 DEFAULT NULL AFTER longitude");
// Populate location from existing lat/lng
// Note: SRID 4326 uses axis order (latitude, longitude) in MySQL 8.0+
$wpdb->query("UPDATE {$table_properties} SET location = ST_PointFromText(CONCAT('POINT(', latitude, ' ', longitude, ')'), 4326) WHERE latitude IS NOT NULL AND longitude IS NOT NULL");
// Make column NOT NULL (required for spatial index)
$wpdb->query("ALTER TABLE {$table_properties} MODIFY location POINT NOT NULL SRID 4326");
// Add spatial index
$existing_indexes = self::get_existing_indexes($table_properties);
if (!isset($existing_indexes['idx_location_spatial'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD SPATIAL INDEX idx_location_spatial (location)");
}
}
update_option('mls_schema_version', 4);
}
// Migration to schema version 5: Add coordinates_invalid column for geo validation
if ($current_schema < 5) {
$table_properties = $wpdb->prefix . MLS_TABLE_PROPERTIES;
// Check if column exists
$column_exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'coordinates_invalid'",
DB_NAME,
$table_properties
));
if (!$column_exists) {
$wpdb->query("ALTER TABLE {$table_properties} ADD COLUMN coordinates_invalid TINYINT(1) NOT NULL DEFAULT 0 AFTER longitude");
}
// Add index if not exists
$existing_indexes = self::get_existing_indexes($table_properties);
if (!isset($existing_indexes['idx_coordinates_invalid'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_coordinates_invalid (coordinates_invalid)");
}
update_option('mls_schema_version', 5);
}
// Future migrations go here:
// if ($current_schema < 4) { ... }
// if ($current_schema < 6) { ... }
}
/**
View File
@@ -0,0 +1,259 @@
<?php
/**
* Geographic Coordinate Validator
*
* Validates that property coordinates fall within their claimed state boundaries.
* Uses bounding box approximations for fast validation.
*/
if (!defined('ABSPATH')) {
exit;
}
class MLS_Geo_Validator {
/**
* State bounding boxes
* Format: state_code => [min_lat, max_lat, min_lng, max_lng]
*
* @var array
*/
private static $state_bounds = null;
/**
* Load state bounds data
*
* @return array
*/
private static function get_state_bounds() {
if (self::$state_bounds === null) {
$bounds_file = MLS_PLUGIN_DIR . 'data/state-bounds.php';
if (file_exists($bounds_file)) {
self::$state_bounds = include $bounds_file;
} else {
self::$state_bounds = array();
}
}
return self::$state_bounds;
}
/**
* Check if coordinates are valid for a given state
*
* @param float $latitude Latitude
* @param float $longitude Longitude
* @param string $state_code Two-letter state code (e.g., 'MN', 'IA')
* @return bool True if coordinates are within state bounds
*/
public static function validate_coordinates($latitude, $longitude, $state_code) {
// Null or empty coordinates are invalid
if ($latitude === null || $longitude === null || $latitude === '' || $longitude === '') {
return false;
}
$lat = (float) $latitude;
$lng = (float) $longitude;
// Basic sanity check - valid lat/lng ranges
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
return false;
}
// Continental US rough bounds check (catches obviously wrong data like European coords)
// This is a quick filter before state-specific validation
if ($lat < 24 || $lat > 50 || $lng < -125 || $lng > -66) {
// Allow Alaska, Hawaii, and territories which fall outside continental bounds
if (!in_array(strtoupper($state_code), array('AK', 'HI', 'PR', 'VI', 'GU'))) {
return false;
}
}
// Get state-specific bounds
$bounds = self::get_state_bounds();
$state_code = strtoupper($state_code);
if (!isset($bounds[$state_code])) {
// Unknown state - can't validate, assume valid
return true;
}
list($min_lat, $max_lat, $min_lng, $max_lng) = $bounds[$state_code];
// Check if coordinates fall within state bounding box
// Add small buffer (0.1 degrees ~= 7 miles) for edge cases
$buffer = 0.1;
return $lat >= ($min_lat - $buffer) &&
$lat <= ($max_lat + $buffer) &&
$lng >= ($min_lng - $buffer) &&
$lng <= ($max_lng + $buffer);
}
/**
* Validate a property record and return whether coordinates are invalid
*
* @param object|array $property Property data with latitude, longitude, state_or_province
* @return int 1 if coordinates are invalid, 0 if valid
*/
public static function check_property($property) {
// Convert to array if object
if (is_object($property)) {
$lat = isset($property->latitude) ? $property->latitude : null;
$lng = isset($property->longitude) ? $property->longitude : null;
$state = isset($property->state_or_province) ? $property->state_or_province : null;
} else {
$lat = isset($property['latitude']) ? $property['latitude'] : null;
$lng = isset($property['longitude']) ? $property['longitude'] : null;
$state = isset($property['state_or_province']) ? $property['state_or_province'] : null;
}
// If no coordinates, mark as invalid for map purposes
if ($lat === null || $lng === null) {
return 1;
}
// If no state, can't validate - assume valid
if (empty($state)) {
return 0;
}
// Normalize state code (handle full names or codes)
$state_code = self::normalize_state_code($state);
// Validate
return self::validate_coordinates($lat, $lng, $state_code) ? 0 : 1;
}
/**
* Normalize state name/code to two-letter code
*
* @param string $state State name or code
* @return string Two-letter state code
*/
private static function normalize_state_code($state) {
$state = trim($state);
// Already a two-letter code
if (strlen($state) === 2) {
return strtoupper($state);
}
// Common state name to code mappings
$name_to_code = array(
'minnesota' => 'MN',
'iowa' => 'IA',
'wisconsin' => 'WI',
'north dakota' => 'ND',
'south dakota' => 'SD',
'nebraska' => 'NE',
'missouri' => 'MO',
'illinois' => 'IL',
// Add more as needed
);
$lower = strtolower($state);
if (isset($name_to_code[$lower])) {
return $name_to_code[$lower];
}
// Return first two characters as fallback
return strtoupper(substr($state, 0, 2));
}
/**
* Validate all existing properties in the database
* Updates coordinates_invalid column for each record
*
* @param callable $progress_callback Optional callback for progress updates
* @return array Results with counts
*/
public static function validate_all_properties($progress_callback = null) {
global $wpdb;
$table = $wpdb->prefix . MLS_TABLE_PROPERTIES;
$results = array(
'total' => 0,
'valid' => 0,
'invalid' => 0,
'errors' => array(),
);
// Get total count
$results['total'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
if ($results['total'] === 0) {
return $results;
}
// Process in batches
$batch_size = 500;
$offset = 0;
while ($offset < $results['total']) {
$properties = $wpdb->get_results($wpdb->prepare(
"SELECT id, latitude, longitude, state_or_province FROM {$table} LIMIT %d OFFSET %d",
$batch_size,
$offset
));
if (empty($properties)) {
break;
}
$valid_ids = array();
$invalid_ids = array();
foreach ($properties as $property) {
$is_invalid = self::check_property($property);
if ($is_invalid) {
$invalid_ids[] = $property->id;
$results['invalid']++;
} else {
$valid_ids[] = $property->id;
$results['valid']++;
}
}
// Batch update valid records
if (!empty($valid_ids)) {
$ids_string = implode(',', array_map('intval', $valid_ids));
$wpdb->query("UPDATE {$table} SET coordinates_invalid = 0 WHERE id IN ({$ids_string})");
}
// Batch update invalid records
if (!empty($invalid_ids)) {
$ids_string = implode(',', array_map('intval', $invalid_ids));
$wpdb->query("UPDATE {$table} SET coordinates_invalid = 1 WHERE id IN ({$ids_string})");
}
$offset += $batch_size;
// Progress callback
if ($progress_callback && is_callable($progress_callback)) {
$progress_callback($offset, $results['total'], $results['valid'], $results['invalid']);
}
}
return $results;
}
/**
* Get statistics about coordinate validity
*
* @return array Statistics
*/
public static function get_stats() {
global $wpdb;
$table = $wpdb->prefix . MLS_TABLE_PROPERTIES;
return array(
'total' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"),
'valid' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE coordinates_invalid = 0"),
'invalid' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE coordinates_invalid = 1"),
'null_coords' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE latitude IS NULL OR longitude IS NULL"),
);
}
}
+5 -3
View File
@@ -375,13 +375,15 @@ class MLS_Image_Endpoint {
exit;
}
// Send headers
// Send headers - remove any no-cache headers WordPress may have added
header_remove('Pragma');
header('Pragma: public');
header('Content-Type: ' . $mime_type);
header('Content-Length: ' . $file_size);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $last_modified) . ' GMT');
header('ETag: "' . $etag . '"');
header('Cache-Control: public, max-age=31536000'); // 1 year
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
header('Cache-Control: public, max-age=3600'); // 1 hour
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600) . ' GMT');
// Stream file
readfile($path);
View File
View File
@@ -109,8 +109,8 @@ class MLS_Query {
}
/**
* Build SQL for distance-based filtering using Haversine formula
* Returns properties within specified miles of a center point
* Build SQL for distance-based filtering using spatial index
* Uses bounding box pre-filter + ST_Distance_Sphere for accuracy
*
* @param float $lat Center latitude
* @param float $lng Center longitude
@@ -118,14 +118,46 @@ class MLS_Query {
* @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
// Convert miles to meters for ST_Distance_Sphere (returns meters)
$meters = $miles * 1609.344;
// Create center point (SRID 4326 uses lat, lng order in MySQL 8.0+)
// Use ST_Distance_Sphere with the spatial indexed location column
return sprintf(
"(3959 * acos(cos(radians(%f)) * cos(radians(latitude)) * cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) <= %f",
"ST_Distance_Sphere(location, ST_PointFromText('POINT(%f %f)', 4326)) <= %f",
$lat,
$lng,
$lat,
$miles
$meters
);
}
/**
* Build SQL for bounding box pre-filter
* Uses simple BETWEEN for fast initial filtering before distance calc
* This narrows down candidates significantly before the expensive ST_Distance_Sphere
*
* @param float $lat Center latitude
* @param float $lng Center longitude
* @param float $miles Radius in miles
* @return string SQL expression for bounding box filter
*/
private function get_bounding_box_filter_sql($lat, $lng, $miles) {
// Approximate degrees per mile (varies by latitude, using average)
// 1 degree latitude ≈ 69 miles
// 1 degree longitude ≈ 69 miles * cos(latitude)
$lat_delta = $miles / 69.0;
$lng_delta = $miles / (69.0 * cos(deg2rad($lat)));
$min_lat = $lat - $lat_delta;
$max_lat = $lat + $lat_delta;
$min_lng = $lng - $lng_delta;
$max_lng = $lng + $lng_delta;
// Use BETWEEN for bounding box - efficient with indexes on lat/lng
return sprintf(
"(latitude BETWEEN %f AND %f AND longitude BETWEEN %f AND %f)",
$min_lat, $max_lat,
$min_lng, $max_lng
);
}
@@ -161,6 +193,7 @@ class MLS_Query {
'search' => null, // Search in address/remarks
'bounds' => null, // Map bounds: array(sw_lat, sw_lng, ne_lat, ne_lng)
'center' => null, // Map center for distance sort: array(lat, lng)
'featured_ids' => null, // Array of listing_id values to prioritize after HomeProz
'limit' => 20,
'offset' => 0,
'orderby' => 'modification_timestamp',
@@ -218,12 +251,18 @@ class MLS_Query {
$values[] = $args['postal_code'];
} elseif ($args['center_lat'] && $args['center_lng']) {
// Direct lat/lng radius search (from homepage location search)
// Use bounding box pre-filter for spatial index, then exact distance
$bbox_filter = $this->get_bounding_box_filter_sql(
(float) $args['center_lat'],
(float) $args['center_lng'],
(int) $args['radius']
);
$distance_filter = $this->get_distance_filter_sql(
(float) $args['center_lat'],
(float) $args['center_lng'],
(int) $args['radius']
);
$where[] = "({$distance_filter})";
$where[] = "({$bbox_filter} AND {$distance_filter})";
}
if ($args['county']) {
@@ -311,6 +350,18 @@ class MLS_Query {
$sql .= ' WHERE ' . implode(' AND ', $where);
// ORDER BY
// Always prioritize: 1) HomeProz listings, 2) Featured listings, 3) Regular listings
// Build featured sort expression if featured_ids provided
$featured_sort = '';
if (!empty($args['featured_ids']) && is_array($args['featured_ids'])) {
$featured_ids = array_map('sanitize_text_field', $args['featured_ids']);
$featured_placeholders = implode(',', array_fill(0, count($featured_ids), '%s'));
$featured_sort = $wpdb->prepare(
", (CASE WHEN listing_id IN ({$featured_placeholders}) THEN 1 ELSE 0 END) DESC",
...$featured_ids
);
}
// If center provided, sort by distance from center
if ($args['center'] && is_array($args['center']) && count($args['center']) === 2) {
list($center_lat, $center_lng) = $args['center'];
@@ -318,7 +369,7 @@ class MLS_Query {
// Using squared Euclidean distance with latitude adjustment for speed
$lat_factor = cos(deg2rad((float) $center_lat));
$sql .= $wpdb->prepare(
" ORDER BY (POW(latitude - %f, 2) + POW((longitude - %f) * %f, 2)) ASC",
" ORDER BY is_homeproz DESC{$featured_sort}, (POW(latitude - %f, 2) + POW((longitude - %f) * %f, 2)) ASC",
(float) $center_lat,
(float) $center_lng,
$lat_factor
@@ -338,7 +389,7 @@ class MLS_Query {
$orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'modification_timestamp';
$order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
$sql .= " ORDER BY {$orderby} {$order}";
$sql .= " ORDER BY is_homeproz DESC{$featured_sort}, {$orderby} {$order}";
}
// LIMIT/OFFSET
@@ -530,13 +581,19 @@ class MLS_Query {
$values[] = $args['postal_code'];
} elseif (!empty($args['center_lat']) && !empty($args['center_lng'])) {
// Direct lat/lng radius search (from homepage location search)
// Use bounding box pre-filter for spatial index, then exact distance
$radius = !empty($args['radius']) ? (int) $args['radius'] : 30;
$bbox_filter = $this->get_bounding_box_filter_sql(
(float) $args['center_lat'],
(float) $args['center_lng'],
$radius
);
$distance_filter = $this->get_distance_filter_sql(
(float) $args['center_lat'],
(float) $args['center_lng'],
$radius
);
$where[] = "({$distance_filter})";
$where[] = "({$bbox_filter} AND {$distance_filter})";
}
if (!empty($args['county'])) {
@@ -728,7 +785,8 @@ class MLS_Query {
$table = $this->db->properties_table();
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
// Exclude properties with invalid coordinates from map bounds
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
$values = array();
// Add state filter (MN and IA only)
+20
View File
@@ -627,6 +627,18 @@ class MLS_Sync_Engine {
$this->emit_progress('property_created', array('listing_key' => $listing_key));
}
// Update spatial location column (wpdb can't handle ST_PointFromText directly)
$lat = $property['Latitude'] ?? null;
$lng = $property['Longitude'] ?? null;
if ($lat !== null && $lng !== null) {
$wpdb->query($wpdb->prepare(
"UPDATE {$this->db->properties_table()} SET location = ST_PointFromText(CONCAT('POINT(', %f, ' ', %f, ')'), 4326) WHERE listing_key = %s",
(float) $lat,
(float) $lng,
$listing_key
));
}
// Process media if present
if (isset($property['Media']) && is_array($property['Media'])) {
$this->media_handler->sync_property_media($listing_key, $property['Media'], false, $this->progress_callback);
@@ -652,6 +664,13 @@ class MLS_Sync_Engine {
* @return array Mapped data for database
*/
private function map_property_data($property) {
// Validate coordinates against state boundaries
$coordinates_invalid = MLS_Geo_Validator::validate_coordinates(
$property['Latitude'] ?? null,
$property['Longitude'] ?? null,
$property['StateOrProvince'] ?? 'MN'
) ? 0 : 1;
return array(
'listing_id' => $property['ListingId'] ?? null,
'originating_system' => $property['OriginatingSystemName'] ?? 'northstar',
@@ -673,6 +692,7 @@ class MLS_Sync_Engine {
'county' => $property['CountyOrParish'] ?? null,
'latitude' => $property['Latitude'] ?? null,
'longitude' => $property['Longitude'] ?? null,
'coordinates_invalid' => $coordinates_invalid,
'property_type' => $property['PropertyType'] ?? null,
'property_sub_type' => $property['PropertySubType'] ?? null,
@@ -97,6 +97,7 @@ final class MLS_Plugin {
require_once MLS_PLUGIN_DIR . 'includes/class-mls-image-endpoint.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-query.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-cluster.php';
require_once MLS_PLUGIN_DIR . 'includes/class-mls-geo-validator.php';
// Activation/Deactivation
require_once MLS_PLUGIN_DIR . 'includes/class-mls-activator.php';
View File