schedule_geocoding($post_id); } public function schedule_geocoding($post_id) { wp_schedule_single_event(time() + 5, 'hvac_geocode_address', [$post_id]); } public function geocode_address($post_id) { // Check rate limiting if (!$this->check_rate_limit()) { // Reschedule for later wp_schedule_single_event(time() + 60, 'hvac_geocode_address', [$post_id]); return; } $address = $this->build_address($post_id); if (empty($address)) { return; } update_post_meta($post_id, '_last_geocode_attempt', time()); // Check cache first $cache_key = 'geocode_' . md5($address); $cached = get_transient($cache_key); if ($cached !== false) { $this->update_coordinates($post_id, $cached); return; } // Make API request $result = $this->make_geocoding_request($address); if ($result && isset($result['lat'], $result['lng'])) { // Cache successful result set_transient($cache_key, $result, self::$cache_duration); $this->update_coordinates($post_id, $result); update_post_meta($post_id, '_geocoding_status', 'success'); update_post_meta($post_id, '_last_geocoded', time()); } else { // Handle failure $this->handle_geocoding_failure($post_id, $result); } } private function build_address($post_id) { $city = get_post_meta($post_id, 'trainer_city', true); $state = get_post_meta($post_id, 'trainer_state', true); $country = get_post_meta($post_id, 'trainer_country', true); $address_parts = array_filter([$city, $state, $country]); return implode(', ', $address_parts); } private function check_rate_limit() { $rate_key = 'hvac_geocoding_rate_' . gmdate('Y-m-d-H-i'); $current_count = get_transient($rate_key) ?: 0; if ($current_count >= self::$rate_limit) { return false; } set_transient($rate_key, $current_count + 1, 60); return true; } private function make_geocoding_request($address) { if (empty(self::$api_key)) { return ['error' => 'No API key configured']; } $url = 'https://maps.googleapis.com/maps/api/geocode/json'; $params = [ 'address' => $address, 'key' => self::$api_key, 'components' => 'country:US|country:CA' // Restrict to North America ]; $response = wp_remote_get($url . '?' . http_build_query($params), [ 'timeout' => 10, 'user-agent' => 'HVAC Trainer Directory/1.0' ]); if (is_wp_error($response)) { return ['error' => $response->get_error_message()]; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (!$data || $data['status'] !== 'OK' || empty($data['results'])) { return ['error' => $data['status'] ?? 'Unknown error']; } $location = $data['results'][0]['geometry']['location']; return [ 'lat' => $location['lat'], 'lng' => $location['lng'], 'formatted_address' => $data['results'][0]['formatted_address'], 'confidence' => $this->calculate_confidence($data['results'][0]) ]; } private function update_coordinates($post_id, $result) { update_post_meta($post_id, 'latitude', $result['lat']); update_post_meta($post_id, 'longitude', $result['lng']); if (isset($result['formatted_address'])) { update_post_meta($post_id, 'formatted_address', $result['formatted_address']); } if (isset($result['confidence'])) { update_post_meta($post_id, 'geocoding_confidence', $result['confidence']); } if (isset($result['source'])) { update_post_meta($post_id, 'geocoding_source', $result['source']); } } private function handle_geocoding_failure($post_id, $error_result) { $error_message = $error_result['error'] ?? 'Unknown error'; update_post_meta($post_id, '_geocoding_status', 'failed'); update_post_meta($post_id, '_geocoding_error', $error_message); // Implement retry logic based on error type switch ($error_message) { case 'OVER_QUERY_LIMIT': // Retry in 1 hour wp_schedule_single_event(time() + HOUR_IN_SECONDS, 'hvac_geocode_address', [$post_id]); break; case 'ZERO_RESULTS': // Try fallback geocoding service $this->try_fallback_geocoding($post_id); break; case 'REQUEST_DENIED': // Log API key issue error_log("HVAC Geocoding: API key issue - {$error_message}"); break; default: // Retry in 5 minutes for transient errors wp_schedule_single_event(time() + 300, 'hvac_geocode_address', [$post_id]); } } private function try_fallback_geocoding($post_id) { // Implement OpenStreetMap Nominatim as fallback $address = $this->build_address($post_id); $url = 'https://nominatim.openstreetmap.org/search'; $params = [ 'q' => $address, 'format' => 'json', 'limit' => 1, 'countrycodes' => 'us,ca' ]; $response = wp_remote_get($url . '?' . http_build_query($params), [ 'timeout' => 10, 'user-agent' => 'HVAC Trainer Directory/1.0' ]); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (!empty($data) && isset($data[0]['lat'], $data[0]['lon'])) { $result = [ 'lat' => floatval($data[0]['lat']), 'lng' => floatval($data[0]['lon']), 'formatted_address' => $data[0]['display_name'], 'confidence' => 0.7, // Lower confidence for fallback 'source' => 'nominatim' ]; $this->update_coordinates($post_id, $result); update_post_meta($post_id, '_geocoding_status', 'success_fallback'); return; } } // Final fallback failed update_post_meta($post_id, '_geocoding_status', 'failed_all'); } private function calculate_confidence($result) { $types = $result['types'] ?? []; // Higher confidence for more specific location types if (in_array('street_address', $types)) return 1.0; if (in_array('premise', $types)) return 0.9; if (in_array('subpremise', $types)) return 0.85; if (in_array('locality', $types)) return 0.8; if (in_array('administrative_area_level_3', $types)) return 0.7; if (in_array('administrative_area_level_2', $types)) return 0.6; if (in_array('administrative_area_level_1', $types)) return 0.5; return 0.4; // Low confidence for country-level matches } // Public methods for manual geocoding public function geocode_profile($profile_id) { return $this->geocode_address($profile_id); } public function get_coordinates($profile_id) { $lat = get_post_meta($profile_id, 'latitude', true); $lng = get_post_meta($profile_id, 'longitude', true); if ($lat && $lng) { return [ 'latitude' => floatval($lat), 'longitude' => floatval($lng), 'formatted_address' => get_post_meta($profile_id, 'formatted_address', true), 'confidence' => get_post_meta($profile_id, 'geocoding_confidence', true), 'source' => get_post_meta($profile_id, 'geocoding_source', true) ?: 'google', 'last_geocoded' => get_post_meta($profile_id, '_last_geocoded', true) ]; } return null; } public function get_geocoding_status($profile_id) { return [ 'status' => get_post_meta($profile_id, '_geocoding_status', true), 'error' => get_post_meta($profile_id, '_geocoding_error', true), 'last_attempt' => get_post_meta($profile_id, '_last_geocode_attempt', true), 'last_success' => get_post_meta($profile_id, '_last_geocoded', true) ]; } public function clear_coordinates($profile_id) { delete_post_meta($profile_id, 'latitude'); delete_post_meta($profile_id, 'longitude'); delete_post_meta($profile_id, 'formatted_address'); delete_post_meta($profile_id, 'geocoding_confidence'); delete_post_meta($profile_id, 'geocoding_source'); delete_post_meta($profile_id, '_geocoding_status'); delete_post_meta($profile_id, '_geocoding_error'); delete_post_meta($profile_id, '_last_geocoded'); } // Proximity search functionality public function find_nearby_profiles($latitude, $longitude, $radius_km = 50, $limit = 20) { global $wpdb; $query = $wpdb->prepare(" SELECT p.ID, p.post_title, lat_meta.meta_value as latitude, lng_meta.meta_value as longitude, ( 6371 * acos( cos(radians(%f)) * cos(radians(CAST(lat_meta.meta_value AS DECIMAL(10,8)))) * cos(radians(CAST(lng_meta.meta_value AS DECIMAL(11,8))) - radians(%f)) + sin(radians(%f)) * sin(radians(CAST(lat_meta.meta_value AS DECIMAL(10,8)))) ) ) AS distance FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} lat_meta ON p.ID = lat_meta.post_id AND lat_meta.meta_key = 'latitude' LEFT JOIN {$wpdb->postmeta} lng_meta ON p.ID = lng_meta.post_id AND lng_meta.meta_key = 'longitude' LEFT JOIN {$wpdb->postmeta} public_meta ON p.ID = public_meta.post_id AND public_meta.meta_key = 'is_public_profile' WHERE p.post_type = 'trainer_profile' AND p.post_status = 'publish' AND lat_meta.meta_value IS NOT NULL AND lng_meta.meta_value IS NOT NULL AND public_meta.meta_value = '1' HAVING distance < %d ORDER BY distance ASC LIMIT %d ", $latitude, $longitude, $latitude, $radius_km, $limit); return $wpdb->get_results($query); } // Bulk geocoding for migration public function bulk_geocode_profiles($limit = 10) { $profiles = get_posts([ 'post_type' => 'trainer_profile', 'posts_per_page' => $limit, 'meta_query' => [ 'relation' => 'AND', [ 'relation' => 'OR', [ 'key' => 'trainer_city', 'compare' => 'EXISTS' ], [ 'key' => 'trainer_state', 'compare' => 'EXISTS' ] ], [ 'key' => 'latitude', 'compare' => 'NOT EXISTS' ] ] ]); $processed = 0; foreach ($profiles as $profile) { if ($this->check_rate_limit()) { $this->geocode_address($profile->ID); $processed++; } else { break; } } return $processed; } } // Initialize the geocoding service HVAC_Geocoding_Service::get_instance();