From 1c728ec60e21e30ed73ef83f9aa6faf30ce2da08 Mon Sep 17 00:00:00 2001 From: "Hanson.xyz Dev" Date: Tue, 16 Dec 2025 00:31:11 -0600 Subject: [PATCH] Fix clustering density with pixel-based grid calculation Replace geohash precision mapping with dynamic grid sizing based on target pixel spacing (60px between cluster centers). Uses Leaflet/OSM tile math to calculate degrees-per-pixel at each zoom level, adjusted for Mercator projection at the viewport's center latitude. At zoom 7, this gives ~52km cells and ~150 clusters statewide, properly separating Minneapolis from St. Cloud. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../includes/class-mls-cluster.php | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php index cfc7649c..c4b87289 100644 --- a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php +++ b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php @@ -18,29 +18,10 @@ class MLS_Cluster { const GEOHASH_CHARS = '0123456789bcdefghjkmnpqrstuvwxyz'; /** - * Zoom level to geohash precision mapping - * Lower precision = larger geographic area = more clustering + * Target pixel spacing between cluster centers + * With 30px cluster icons, 45px gap gives 75px total spacing */ - const ZOOM_PRECISION = array( - 3 => 2, // ~630km cells - continent level - 4 => 2, - 5 => 2, - 6 => 3, // ~78km cells - state level - 7 => 3, - 8 => 3, - 9 => 4, // ~20km cells - county level - 10 => 4, - 11 => 4, - 12 => 5, // ~2.4km cells - city level - 13 => 5, - 14 => 5, - 15 => 6, // ~610m cells - neighborhood level - 16 => 6, - 17 => 6, - 18 => 7, // ~76m cells - street level - 19 => 7, - 20 => 8, // Individual points - ); + const CLUSTER_PIXEL_SPACING = 60; /** * Maximum properties to return as individual markers @@ -156,20 +137,32 @@ class MLS_Cluster { } /** - * Get precision for zoom level + * Calculate grid cell size in degrees for a given zoom level + * + * Uses Leaflet/OSM tile math to determine what geographic distance + * corresponds to CLUSTER_PIXEL_SPACING pixels at the given zoom. * * @param int $zoom Map zoom level (1-20) - * @return int Geohash precision + * @param float $lat Center latitude (affects Mercator projection) + * @return array [lat_step, lng_step] in degrees */ - public function get_precision_for_zoom($zoom) { + public function get_grid_step_for_zoom($zoom, $lat = 45.0) { $zoom = max(3, min(20, (int) $zoom)); - if (isset(self::ZOOM_PRECISION[$zoom])) { - return self::ZOOM_PRECISION[$zoom]; - } + // Degrees per pixel at zoom level (longitude) + // 360 degrees / (256 pixels * 2^zoom tiles) + $degrees_per_pixel_lng = 360.0 / (256 * pow(2, $zoom)); - // Default to precision 5 for unknown zoom levels - return 5; + // Latitude degrees per pixel (adjusted for Mercator at given latitude) + // At the equator it's the same, at poles it's compressed + $lat_rad = deg2rad(abs($lat)); + $degrees_per_pixel_lat = $degrees_per_pixel_lng * cos($lat_rad); + + // Calculate step size to achieve target pixel spacing + $lng_step = self::CLUSTER_PIXEL_SPACING * $degrees_per_pixel_lng; + $lat_step = self::CLUSTER_PIXEL_SPACING * $degrees_per_pixel_lat; + + return array($lat_step, $lng_step); } /** @@ -258,14 +251,18 @@ class MLS_Cluster { } // If low count or high zoom, return individual markers - $precision = $this->get_precision_for_zoom($args['zoom']); - - if ($total <= self::MAX_INDIVIDUAL_MARKERS || $args['zoom'] >= 18) { + if ($total <= self::MAX_INDIVIDUAL_MARKERS || $args['zoom'] >= 16) { return $this->get_individual_markers($where_sql, $values, $total); } + // Calculate center latitude for Mercator adjustment + $center_lat = 45.0; // Default Minnesota + if ($args['bounds'] && count($args['bounds']) === 4) { + $center_lat = ($args['bounds'][0] + $args['bounds'][2]) / 2; + } + // Return clusters - return $this->get_cluster_data($where_sql, $values, $precision, $total); + return $this->get_cluster_data($where_sql, $values, $args['zoom'], $center_lat, $total); } /** @@ -334,27 +331,22 @@ class MLS_Cluster { } /** - * Get clustered data using geohash grouping + * Get clustered data using grid-based grouping * * @param string $where_sql WHERE clause * @param array $values Prepared values - * @param int $precision Geohash precision + * @param int $zoom Map zoom level + * @param float $center_lat Center latitude for Mercator adjustment * @param int $total Total count * @return array */ - private function get_cluster_data($where_sql, $values, $precision, $total) { + private function get_cluster_data($where_sql, $values, $zoom, $center_lat, $total) { global $wpdb; $table = $this->db->properties_table(); - // Use MySQL SUBSTR to extract geohash prefix for grouping - // We compute geohash on-the-fly using latitude/longitude - // For better performance with 30k+ records, we use grid-based clustering - - // Calculate grid cell size based on precision - // Approximate degrees per geohash precision level - $lat_step = 180 / pow(2, (int)($precision * 2.5)); - $lng_step = 360 / pow(2, (int)($precision * 2.5)); + // Calculate grid cell size based on zoom level and target pixel spacing + list($lat_step, $lng_step) = $this->get_grid_step_for_zoom($zoom, $center_lat); // Use FLOOR to group coordinates into cells $sql = "SELECT @@ -388,7 +380,8 @@ class MLS_Cluster { 'type' => 'clusters', 'total' => $total, 'cluster_count' => count($clusters), - 'precision' => $precision, + 'zoom' => $zoom, + 'grid_size_deg' => array('lat' => $lat_step, 'lng' => $lng_step), 'clusters' => $clusters, ); }