# Search-Location Integration Plan - ThrillWiki ## Executive Summary This document outlines the comprehensive integration strategy for enhancing ThrillWiki's existing search system with location capabilities. The plan builds upon the current django-filters based search architecture and integrates it with the designed domain-specific location models (ParkLocation, RideLocation, CompanyHeadquarters) and unified map service. ## Current State Analysis ### Existing Search Architecture - **Framework**: Django-filters with [`ParkFilter`](parks/filters.py:26) and [`RideSearchView`](rides/views.py:1) - **Current Capabilities**: Text search, status filtering, operator filtering, date ranges, numeric filters - **Performance**: Basic queryset optimization with select_related/prefetch_related - **UI**: HTMX-driven filtered results with [`AdaptiveSearchView`](core/views/search.py:5) - **Templates**: Structured template hierarchy in [`templates/core/search/`](templates/core/search/) ### Location System State - **Current**: Hybrid system with both generic Location model and domain-specific models - **Designed**: Complete transition to [`ParkLocation`](memory-bank/features/location-models-design.md:17), [`RideLocation`](memory-bank/features/location-models-design.md:213), [`CompanyHeadquarters`](memory-bank/features/location-models-design.md:317) - **Spatial Features**: PostGIS with PointField, spatial indexing, distance calculations - **Map Integration**: [`UnifiedMapService`](memory-bank/features/map-service-design.md:35) designed for clustering and filtering ## 1. Search Index Enhancement Plan ### 1.1 Location Field Integration #### Current Search Fields Extension ```python # Enhanced ParkFilter in parks/filters.py class LocationEnhancedParkFilter(ParkFilter): search_fields = [ 'name__icontains', 'description__icontains', 'park_location__city__icontains', # Domain-specific model 'park_location__state__icontains', 'park_location__country__icontains', 'park_location__highway_exit__icontains', # Road trip specific 'park_location__postal_code__icontains' ] ``` #### Spatial Search Fields ```python # New spatial search capabilities class SpatialSearchMixin: near_point = PointFilter( method='filter_near_point', help_text="Search within radius of coordinates" ) within_radius = NumberFilter( method='filter_within_radius', help_text="Radius in miles (used with near_point)" ) within_bounds = BoundsFilter( method='filter_within_bounds', help_text="Search within geographic bounding box" ) ``` #### Index Performance Strategy - **Spatial Indexes**: Maintain GIST indexes on all PointField columns - **Composite Indexes**: Add indexes on frequently combined fields (city+state, country+state) - **Text Search Optimization**: Consider adding GIN indexes for full-text search if performance degrades ### 1.2 Cross-Domain Location Indexing #### Unified Location Search Index ```python class UnifiedLocationSearchService: """Service for searching across all location-enabled models""" def search_all_locations(self, query: str, location_types: Set[str] = None): results = [] # Parks via ParkLocation if not location_types or 'park' in location_types: park_results = self._search_parks(query) results.extend(park_results) # Rides via RideLocation (optional) if not location_types or 'ride' in location_types: ride_results = self._search_rides(query) results.extend(ride_results) # Companies via CompanyHeadquarters if not location_types or 'company' in location_types: company_results = self._search_companies(query) results.extend(company_results) return self._rank_and_sort_results(results) ``` ## 2. Spatial Search Architecture ### 2.1 Geographic Query Patterns #### Distance-Based Search ```python class DistanceSearchMixin: """Mixin for distance-based filtering""" def filter_near_point(self, queryset, name, value): """Filter by proximity to a point""" if not value or not hasattr(value, 'coords'): return queryset radius_miles = self.data.get('within_radius', 50) # Default 50 miles from django.contrib.gis.measure import D return queryset.filter( park_location__point__distance_lte=( value, D(mi=radius_miles) ) ).annotate( distance=Distance('park_location__point', value) ).order_by('distance') def filter_within_bounds(self, queryset, name, value): """Filter within geographic bounding box""" if not value or not hasattr(value, 'extent'): return queryset return queryset.filter( park_location__point__within=value.extent ) ``` #### Geocoding Integration Pattern ```python class GeocodingSearchMixin: """Handle address-to-coordinate conversion in search""" def filter_near_address(self, queryset, name, value): """Search near an address (geocoded to coordinates)""" if not value: return queryset # Use OpenStreetMap Nominatim for geocoding coordinates = self._geocode_address(value) if not coordinates: # Graceful fallback to text search return self._fallback_text_search(queryset, value) return self.filter_near_point(queryset, name, coordinates) def _geocode_address(self, address: str) -> Optional[Point]: """Convert address to coordinates with caching""" cache_key = f"geocode:{hash(address)}" cached = cache.get(cache_key) if cached: return cached # Implementation using Nominatim API result = nominatim_geocode(address) if result: point = Point(result['lon'], result['lat']) cache.set(cache_key, point, timeout=86400) # 24 hours return point return None ``` ### 2.2 Database Query Optimization #### Optimized Spatial Queries ```python class SpatialQueryOptimizer: """Optimize spatial queries for performance""" def get_optimized_queryset(self, base_queryset, spatial_filters): """Apply optimizations based on query type""" # Use spatial index hints for PostgreSQL queryset = base_queryset.extra( select={'spatial_distance': 'ST_Distance(park_location.point, %s)'}, select_params=[spatial_filters.get('reference_point')] ) if spatial_filters.get('reference_point') else base_queryset # Limit radius searches to reasonable bounds max_radius = min(spatial_filters.get('radius', 100), 500) # Cap at 500 miles # Pre-filter with bounding box before distance calculation if spatial_filters.get('reference_point'): # Create bounding box slightly larger than radius for pre-filtering bbox = self._create_bounding_box( spatial_filters['reference_point'], max_radius * 1.1 ) queryset = queryset.filter(park_location__point__within=bbox) return queryset ``` ## 3. "Near Me" Functionality Design ### 3.1 Geolocation Integration #### Frontend Geolocation Handling ```javascript class LocationSearchService { constructor() { this.userLocation = null; this.locationPermission = 'prompt'; } async requestUserLocation() { try { if (!navigator.geolocation) { throw new Error('Geolocation not supported'); } const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( resolve, reject, { enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 // 5 minutes } ); }); this.userLocation = { lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy }; this.locationPermission = 'granted'; return this.userLocation; } catch (error) { this.locationPermission = 'denied'; await this._handleLocationError(error); return null; } } async _handleLocationError(error) { switch(error.code) { case error.PERMISSION_DENIED: await this._tryIPLocationFallback(); break; case error.POSITION_UNAVAILABLE: this._showLocationUnavailableMessage(); break; case error.TIMEOUT: this._showTimeoutMessage(); break; } } } ``` #### Privacy and Permission Handling ```python class LocationPrivacyMixin: """Handle location privacy concerns""" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Add location permission context context.update({ 'location_features_available': True, 'privacy_policy_url': reverse('privacy'), 'location_consent_required': True, 'ip_location_available': self._ip_location_available(), }) return context def _ip_location_available(self) -> bool: """Check if IP-based location estimation is available""" # Could integrate with MaxMind GeoIP or similar service return hasattr(self.request, 'META') and 'HTTP_CF_IPCOUNTRY' in self.request.META ``` ### 3.2 Fallback Strategies #### IP-Based Location Approximation ```python class IPLocationService: """Fallback location service using IP geolocation""" def get_approximate_location(self, request) -> Optional[Dict]: """Get approximate location from IP address""" try: # Try Cloudflare country header first country = request.META.get('HTTP_CF_IPCOUNTRY') if country and country != 'XX': return self._country_to_coordinates(country) # Fallback to GeoIP database ip_address = self._get_client_ip(request) return self._geoip_lookup(ip_address) except Exception: return None def _country_to_coordinates(self, country_code: str) -> Dict: """Convert country code to approximate center coordinates""" country_centers = { 'US': {'lat': 39.8283, 'lng': -98.5795, 'accuracy': 'country'}, 'CA': {'lat': 56.1304, 'lng': -106.3468, 'accuracy': 'country'}, # Add more countries as needed } return country_centers.get(country_code.upper()) ``` ## 4. Location-Based Filtering Integration ### 4.1 Enhanced Filter Integration #### Geographic Region Filters ```python class GeographicFilterMixin: """Add geographic filtering to existing filter system""" # State/Province filtering state = ModelChoiceFilter( field_name='park_location__state', queryset=None, # Dynamically populated empty_label='Any state/province', method='filter_by_state' ) # Country filtering country = ModelChoiceFilter( field_name='park_location__country', queryset=None, # Dynamically populated empty_label='Any country', method='filter_by_country' ) # Metropolitan area clustering metro_area = ChoiceFilter( method='filter_by_metro_area', choices=[] # Dynamically populated ) def filter_by_metro_area(self, queryset, name, value): """Filter by predefined metropolitan areas""" metro_definitions = { 'orlando': { 'center': Point(-81.3792, 28.5383), 'radius_miles': 30 }, 'los_angeles': { 'center': Point(-118.2437, 34.0522), 'radius_miles': 50 }, # Add more metropolitan areas } metro = metro_definitions.get(value) if not metro: return queryset from django.contrib.gis.measure import D return queryset.filter( park_location__point__distance_lte=( metro['center'], D(mi=metro['radius_miles']) ) ) ``` #### Performance-Optimized Filtering ```python class OptimizedLocationFilter(GeographicFilterMixin, ParkFilter): """Location filtering with performance optimizations""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dynamically populate geographic choices based on available data self._populate_geographic_choices() def _populate_geographic_choices(self): """Populate geographic filter choices efficiently""" # Cache geographic options for performance cache_key = 'location_filter_choices' cached_choices = cache.get(cache_key) if not cached_choices: # Query distinct values efficiently states = ParkLocation.objects.values_list( 'state', flat=True ).distinct().order_by('state') countries = ParkLocation.objects.values_list( 'country', flat=True ).distinct().order_by('country') cached_choices = { 'states': [(s, s) for s in states if s], 'countries': [(c, c) for c in countries if c] } cache.set(cache_key, cached_choices, timeout=3600) # 1 hour # Update filter choices self.filters['state'].extra['choices'] = cached_choices['states'] self.filters['country'].extra['choices'] = cached_choices['countries'] ``` ## 5. Query Integration Patterns ### 5.1 Hybrid Search Scoring #### Relevance + Proximity Scoring ```python class HybridSearchRanking: """Combine text relevance with geographic proximity""" def rank_results(self, queryset, search_query: str, user_location: Point = None): """Apply hybrid ranking algorithm""" # Base text relevance scoring queryset = queryset.annotate( text_rank=Case( When(name__iexact=search_query, then=Value(100)), When(name__icontains=search_query, then=Value(80)), When(description__icontains=search_query, then=Value(60)), When(park_location__city__icontains=search_query, then=Value(40)), default=Value(20), output_field=IntegerField() ) ) # Add proximity scoring if user location available if user_location: queryset = queryset.annotate( distance_miles=Distance('park_location__point', user_location), proximity_rank=Case( When(distance_miles__lt=25, then=Value(50)), # Very close When(distance_miles__lt=100, then=Value(30)), # Close When(distance_miles__lt=300, then=Value(10)), # Regional default=Value(0), output_field=IntegerField() ) ) # Combined score: text relevance + proximity bonus queryset = queryset.annotate( combined_rank=F('text_rank') + F('proximity_rank') ).order_by('-combined_rank', 'distance_miles') else: queryset = queryset.order_by('-text_rank', 'name') return queryset ``` ### 5.2 Cross-Domain Location Search #### Unified Search Across Entities ```python class UnifiedLocationSearchView(TemplateView): """Search across parks, rides, and companies with location context""" template_name = "core/search/unified_results.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) query = self.request.GET.get('q', '') location_types = self.request.GET.getlist('types', ['park', 'ride', 'company']) user_location = self._get_user_location() results = { 'parks': [], 'rides': [], 'companies': [], 'unified': [] } # Search each entity type with location context if 'park' in location_types: results['parks'] = self._search_parks(query, user_location) if 'ride' in location_types: results['rides'] = self._search_rides(query, user_location) if 'company' in location_types: results['companies'] = self._search_companies(query, user_location) # Create unified ranked results results['unified'] = self._create_unified_results(results, user_location) context.update({ 'results': results, 'search_query': query, 'user_location': user_location, 'total_results': sum(len(r) for r in results.values() if isinstance(r, list)) }) return context ``` ## 6. Geocoding Integration Strategy ### 6.1 OpenStreetMap Nominatim Integration #### Geocoding Service Implementation ```python class GeocodingService: """Geocoding service using OpenStreetMap Nominatim""" def __init__(self): self.base_url = "https://nominatim.openstreetmap.org" self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'ThrillWiki/1.0 (contact@thrillwiki.com)' }) def geocode_address(self, address: str, country_bias: str = None) -> Optional[Dict]: """Convert address to coordinates""" cache_key = f"geocode:{hashlib.md5(address.encode()).hexdigest()}" cached = cache.get(cache_key) if cached: return cached params = { 'q': address, 'format': 'json', 'limit': 1, 'addressdetails': 1, 'extratags': 1 } if country_bias: params['countrycodes'] = country_bias try: response = self.session.get( f"{self.base_url}/search", params=params, timeout=5 ) response.raise_for_status() results = response.json() if results: result = results[0] geocoded = { 'lat': float(result['lat']), 'lng': float(result['lon']), 'display_name': result['display_name'], 'confidence': float(result.get('importance', 0.5)), 'address_components': result.get('address', {}) } # Cache successful results for 7 days cache.set(cache_key, geocoded, timeout=604800) return geocoded except (requests.RequestException, ValueError, KeyError) as e: logger.warning(f"Geocoding failed for '{address}': {e}") # Cache failed attempts for 1 hour to prevent repeated API calls cache.set(cache_key, None, timeout=3600) return None def reverse_geocode(self, lat: float, lng: float) -> Optional[Dict]: """Convert coordinates to address""" cache_key = f"reverse_geocode:{lat:.4f},{lng:.4f}" cached = cache.get(cache_key) if cached: return cached params = { 'lat': lat, 'lon': lng, 'format': 'json', 'addressdetails': 1 } try: response = self.session.get( f"{self.base_url}/reverse", params=params, timeout=5 ) response.raise_for_status() result = response.json() if result: address = { 'display_name': result['display_name'], 'components': result.get('address', {}) } cache.set(cache_key, address, timeout=604800) # 7 days return address except (requests.RequestException, ValueError) as e: logger.warning(f"Reverse geocoding failed for {lat},{lng}: {e}") return None ``` ### 6.2 Search Query Enhancement #### Intelligent Address Detection ```python class SmartQueryProcessor: """Detect and process different types of search queries""" def __init__(self): self.geocoding_service = GeocodingService() def process_search_query(self, query: str) -> Dict: """Analyze query and determine search strategy""" query_analysis = { 'original_query': query, 'is_address': self._looks_like_address(query), 'is_coordinates': self._looks_like_coordinates(query), 'has_location_keywords': self._has_location_keywords(query), 'processed_query': query, 'geocoded_location': None, 'search_strategy': 'text' } if query_analysis['is_coordinates']: coords = self._parse_coordinates(query) if coords: query_analysis['geocoded_location'] = coords query_analysis['search_strategy'] = 'spatial' elif query_analysis['is_address'] or query_analysis['has_location_keywords']: geocoded = self.geocoding_service.geocode_address(query) if geocoded: query_analysis['geocoded_location'] = geocoded query_analysis['search_strategy'] = 'spatial_text_hybrid' return query_analysis def _looks_like_address(self, query: str) -> bool: """Detect if query looks like an address""" address_patterns = [ r'\d+\s+\w+\s+(street|st|avenue|ave|road|rd|boulevard|blvd)', r'\w+,\s*\w+\s*\d{5}', # City, State ZIP r'\w+,\s*\w+,\s*\w+', # City, State, Country ] return any(re.search(pattern, query, re.IGNORECASE) for pattern in address_patterns) def _looks_like_coordinates(self, query: str) -> bool: """Detect if query contains coordinates""" coord_pattern = r'-?\d+\.?\d*\s*,\s*-?\d+\.?\d*' return bool(re.search(coord_pattern, query)) ``` ## 7. Performance Optimization Strategy ### 7.1 Database Optimization #### Spatial Index Strategy ```sql -- Essential indexes for location-enhanced search CREATE INDEX CONCURRENTLY idx_park_location_point_gist ON parks_parklocation USING GIST (point); CREATE INDEX CONCURRENTLY idx_park_location_city_state ON parks_parklocation (city, state); CREATE INDEX CONCURRENTLY idx_park_location_country ON parks_parklocation (country); -- Composite indexes for common filter combinations CREATE INDEX CONCURRENTLY idx_park_status_location ON parks_park (status) INCLUDE (id) WHERE status = 'OPERATING'; -- Partial indexes for performance CREATE INDEX CONCURRENTLY idx_ride_location_point_not_null ON rides_ridelocation USING GIST (entrance_point) WHERE entrance_point IS NOT NULL; ``` #### Query Optimization Patterns ```python class SpatialQueryOptimizer: """Optimize spatial queries for performance""" def optimize_distance_query(self, queryset, reference_point, max_radius_miles=100): """Optimize distance-based queries with bounding box pre-filtering""" # Create bounding box for initial filtering (much faster than distance calc) bbox = self._create_bounding_box(reference_point, max_radius_miles) # Pre-filter with bounding box, then apply precise distance filter return queryset.filter( park_location__point__within=bbox ).filter( park_location__point__distance_lte=(reference_point, D(mi=max_radius_miles)) ).annotate( distance=Distance('park_location__point', reference_point) ).order_by('distance') def optimize_multi_location_query(self, park_qs, ride_qs, bounds=None): """Optimize queries across multiple location types""" # Use common table expressions for complex spatial queries if bounds: # Apply spatial filtering early park_qs = park_qs.filter(park_location__point__within=bounds) ride_qs = ride_qs.filter(ride_location__entrance_point__within=bounds) # Use union for combining different location types efficiently return park_qs.union(ride_qs, all=False) ``` ### 7.2 Caching Strategy #### Multi-Level Caching Architecture ```python class LocationSearchCache: """Caching strategy for location-enhanced search""" CACHE_TIMEOUTS = { 'geocoding': 604800, # 7 days 'search_results': 300, # 5 minutes 'location_filters': 3600, # 1 hour 'spatial_index': 86400, # 24 hours } def cache_search_results(self, cache_key: str, results: QuerySet, user_location: Point = None): """Cache search results with location context""" # Include location context in cache key if user_location: location_hash = hashlib.md5( f"{user_location.x:.4f},{user_location.y:.4f}".encode() ).hexdigest()[:8] cache_key = f"{cache_key}_loc_{location_hash}" # Cache serialized results serialized = self._serialize_results(results) cache.set( cache_key, serialized, timeout=self.CACHE_TIMEOUTS['search_results'] ) def get_cached_search_results(self, cache_key: str, user_location: Point = None): """Retrieve cached search results""" if user_location: location_hash = hashlib.md5( f"{user_location.x:.4f},{user_location.y:.4f}".encode() ).hexdigest()[:8] cache_key = f"{cache_key}_loc_{location_hash}" cached = cache.get(cache_key) return self._deserialize_results(cached) if cached else None def invalidate_location_cache(self, location_type: str = None): """Invalidate location-related caches when data changes""" patterns = [ 'search_results_*', 'location_filters', 'geocoding_*' ] if location_type: patterns.append(f'{location_type}_search_*') # Use cache versioning for efficient invalidation for pattern in patterns: cache.delete_pattern(pattern) ``` ### 7.3 Performance Monitoring #### Search Performance Metrics ```python class SearchPerformanceMonitor: """Monitor search performance and spatial query efficiency""" def __init__(self): self.metrics_logger = logging.getLogger('search.performance') def track_search_query(self, query_type: str, query_params: Dict, execution_time: float, result_count: int): """Track search query performance""" metrics = { 'query_type': query_type, 'has_spatial_filter': bool(query_params.get('user_location')), 'has_text_search': bool(query_params.get('search')), 'filter_count': len([k for k, v in query_params.items() if v]), 'execution_time_ms': execution_time * 1000, 'result_count': result_count, 'timestamp': timezone.now().isoformat() } # Log performance data self.metrics_logger.info(json.dumps(metrics)) # Alert on slow queries if execution_time > 1.0: # Queries over 1 second self._alert_slow_query(metrics) def _alert_slow_query(self, metrics: Dict): """Alert on performance issues""" # Implementation for alerting system pass ``` ## 8. API Enhancement Design ### 8.1 Location-Aware Search Endpoints #### Enhanced Search API ```python class LocationSearchAPIView(APIView): """REST API for location-enhanced search""" def get(self, request): """ Enhanced search endpoint with location capabilities Query Parameters: - q: Search query (text) - lat, lng: User coordinates for proximity search - radius: Search radius in miles (default: 50) - bounds: Geographic bounding box (format: north,south,east,west) - types: Entity types to search (park,ride,company) - filters: Additional filters (status, operator, etc.) """ try: search_params = self._parse_search_params(request.GET) results = self._execute_search(search_params) return Response({ 'status': 'success', 'data': { 'results': results['items'], 'total_count': results['total'], 'search_params': search_params, 'has_more': results['has_more'] }, 'meta': { 'query_time_ms': results['execution_time'] * 1000, 'cache_hit': results['from_cache'], 'location_used': bool(search_params.get('user_location')) } }) except ValidationError as e: return Response({ 'status': 'error', 'error': 'Invalid search parameters', 'details': str(e) }, status=400) def _parse_search_params(self, params: QueryDict) -> Dict: """Parse and validate search parameters""" # Parse user location user_location = None if params.get('lat') and params.get('lng'): try: lat = float(params['lat']) lng = float(params['lng']) if -90 <= lat <= 90 and -180 <= lng <= 180: user_location = Point(lng, lat) except (ValueError, TypeError): raise ValidationError("Invalid coordinates") # Parse bounding box bounds = None if params.get('bounds'): try: north, south, east, west = map(float, params['bounds'].split(',')) bounds = Polygon.from_bbox((west, south, east, north)) except (ValueError, TypeError): raise ValidationError("Invalid bounds format") return { 'query': params.get('q', '').strip(), 'user_location': user_location, 'radius_miles': min(float(params.get('radius', 50)), 500), # Cap at 500 miles 'bounds': bounds, 'entity_types': params.getlist('types') or ['park'], 'filters': self._parse_filters(params), 'page': int(params.get('page', 1)), 'page_size': min(int(params.get('page_size', 20)), 100) # Cap at 100 } ``` #### Autocomplete API with Location Context ```python class LocationAutocompleteAPIView(APIView): """Autocomplete API with location awareness""" def get(self, request): """ Location-aware autocomplete for search queries Returns suggestions based on: 1. Entity names (parks, rides, companies) 2. Location names (cities, states, countries) 3. Address suggestions """ query = request.GET.get('q', '').strip() if len(query) < 2: return Response({'suggestions': []}) user_location = self._parse_user_location(request.GET) suggestions = [] # Entity name suggestions (with location context for ranking) entity_suggestions = self._get_entity_suggestions(query, user_location) suggestions.extend(entity_suggestions) # Location name suggestions location_suggestions = self._get_location_suggestions(query) suggestions.extend(location_suggestions) # Address suggestions (via geocoding) if self._looks_like_address(query): address_suggestions = self._get_address_suggestions(query) suggestions.extend(address_suggestions) # Rank and limit suggestions ranked_suggestions = self._rank_suggestions(suggestions, user_location)[:10] return Response({ 'suggestions': ranked_suggestions, 'query': query }) ``` ### 8.2 Enhanced Response Formats #### Unified Location Response Format ```python class LocationSearchResultSerializer(serializers.Serializer): """Unified serializer for location-enhanced search results""" id = serializers.CharField() type = serializers.CharField() # 'park', 'ride', 'company' name = serializers.CharField() slug = serializers.CharField() # Location data location = serializers.SerializerMethodField() # Entity-specific data entity_data = serializers.SerializerMethodField() # Search relevance relevance_score = serializers.FloatField(required=False) distance_miles = serializers.FloatField(required=False) def get_location(self, obj): """Get unified location data""" if hasattr(obj, 'park_location'): location = obj.park_location return { 'coordinates': [location.point.y, location.point.x] if location.point else None, 'address': location.formatted_address, 'city': location.city, 'state': location.state, 'country': location.country, 'highway_exit': location.highway_exit } elif hasattr(obj, 'ride_location'): location = obj.ride_location return { 'coordinates': [location.entrance_point.y, location.entrance_point.x] if location.entrance_point else None, 'park_area': location.park_area, 'park_location': self._get_park_location(obj.park) } # Add other location types... return None def get_entity_data(self, obj): """Get entity-specific data based on type""" if obj._meta.model_name == 'park': return { 'status': obj.status, 'operator': obj.operating_company.name if obj.operating_company else None, 'ride_count': getattr(obj, 'ride_count', 0), 'coaster_count': getattr(obj, 'coaster_count', 0), 'website': obj.website, 'opening_date': obj.opening_date } elif obj._meta.model_name == 'ride': return { 'category': obj.category, 'park': { 'name': obj.park.name, 'slug': obj.park.slug }, 'manufacturer': obj.ride_model.manufacturer.name if obj.ride_model and obj.ride_model.manufacturer else None, 'opening_date': obj.opening_date } # Add other entity types... return {} ``` ## 9. User Experience Design ### 9.1 Search Interface Enhancements #### Location-Aware Search Form ```html
``` #### Location Permission Handling ```javascript class LocationPermissionManager { constructor() { this.permissionStatus = 'unknown'; this.setupEventHandlers(); } setupEventHandlers() { // Location button click handler document.getElementById('use-my-location').addEventListener('click', () => this.requestLocation()); // Privacy-conscious permission checking if ('permissions' in navigator) { navigator.permissions.query({name: 'geolocation'}) .then(permission => { this.permissionStatus = permission.state; this.updateLocationButton(); permission.addEventListener('change', () => { this.permissionStatus = permission.state; this.updateLocationButton(); }); }); } } async requestLocation() { // Show privacy notice if first time if (this.permissionStatus === 'prompt') { const consent = await this.showPrivacyConsent(); if (!consent) return; } try { this.showLocationLoading(); const location = await this.getCurrentPosition(); this.handleLocationSuccess(location); } catch (error) { this.handleLocationError(error); } } showPrivacyConsent() { return new Promise(resolve => { const modal = document.createElement('div'); modal.className = 'location-consent-modal'; modal.innerHTML = ` `; modal.addEventListener('click', (e) => { if (e.target.dataset.action === 'allow') { resolve(true); } else if (e.target.dataset.action === 'deny') { resolve(false); } modal.remove(); }); document.body.appendChild(modal); }); } } ``` ### 9.2 Results Display with Location Context #### Distance-Enhanced Results ```html