Points of Interest (POI)
Points of Interest (POIs) represent geographic locations used throughout the application. POIs are global entities shared across all markets, replacing the legacy city string approach with a normalized, searchable database.
When to Use
Section titled “When to Use”- Reference cities, attractions, or landmarks in itineraries
- Build location pickers in admin forms
- Store geographic data with coordinates and images
- Import locations from Google Places API
POI Types
Section titled “POI Types”POIs are classified by the PoiType enum:
| Type | Value | Icon | Use Case |
|---|---|---|---|
| City | city | building-office-2 | Metropolitan areas, tour stops |
| Attraction | attraction | star | Museums, theme parks, tourist sites |
| Landmark | landmark | map-pin | Churches, monuments, historic buildings |
| NaturalFeature | natural_feature | globe-americas | Parks, mountains, natural wonders |
Source: backend/app/Enums/PoiType.php
Data Model
Section titled “Data Model”Table: pois
| Column | Type | Purpose |
|---|---|---|
| name | varchar(200) | Display name |
| type | varchar(50) | PoiType enum value |
| provider | varchar(50) | Data source (e.g., google_places) |
| provider_id | varchar(255) | External ID for deduplication |
| country | varchar(100) | Full country name |
| country_code | char(2) | ISO country code |
| region | varchar(100) | Region/state within the country (nullable) |
| latitude | decimal(10,8) | Geographic latitude |
| longitude | decimal(11,8) | Geographic longitude |
| description | text | Editorial description |
| image_path | varchar(500) | Storage path for image |
| raw_provider_data | jsonb | Original provider response |
| status | boolean | Active/inactive flag |
| sort_order | int | Display ordering |
Indexes
Section titled “Indexes”| Columns | Purpose |
|---|---|
| (type, status) | Filter by type and active status |
| (country_code, status) | Filter by country |
| name | Name search |
| (provider, provider_id) WHERE provider IS NOT NULL | Unique provider constraint |
Source: backend/database/migrations/2025_12_30_074755_create_pois_table.php
Model Scopes
Section titled “Model Scopes”// Get only active POIsPoi::active()->get();
// Filter by typePoi::ofType(PoiType::City)->get();Poi::ofType('landmark')->get();
// Convenience scopesPoi::cities()->get();Poi::attractions()->get();Source: backend/app/Models/Poi.php:77-116
Model Methods
Section titled “Model Methods”$poi->getDisplayName(); // "Barcelona (Spain)" or "Barcelona"$poi->getImageUrl(); // Full URL to image or null$poi->isFromProvider(); // true if imported from external sourceSource: backend/app/Models/Poi.php:121-148
PoiSelect Component
Section titled “PoiSelect Component”Reusable Filament select field with search and caching:
use App\Filament\Components\Fields\PoiSelect;
// Basic usage (all POI types)PoiSelect::make('poi_id')
// Filter by specific typesPoiSelect::types([PoiType::City, PoiType::Attraction])->select('poi_id')
// Cities onlyPoiSelect::types([PoiType::City])->select('destination_id')Helper Methods
Section titled “Helper Methods”// Get POI name for displayPoiSelect::getPoiName($poiId); // "Barcelona"PoiSelect::getDisplayName($poiId); // "Barcelona (Spain)"
// Clear cached optionsPoiSelect::clearCache();Source: backend/app/Filament/Components/Fields/PoiSelect.php
Caching
Section titled “Caching”POI options are cached for 1 hour (3600 seconds) with cache keys:
poi_select_options_all- All active POIspoi_select_options_city- Cities onlypoi_select_options_city_attraction- Cities and attractions
Admin Panel
Section titled “Admin Panel”Path: Admin > Markets > Points of Interest
Features:
- CRUD operations for all POI types
- Google Places search integration in create/edit forms
- Filters by type, status, and provider
- Bulk delete action
Source: backend/app/Filament/Resources/Pois/PoiResource.php
Data Migration
Section titled “Data Migration”Initial POI data was migrated from the airports table:
- Extracted 8,174 unique city + country combinations
- Used average coordinates for cities with multiple airports
- Set
provider = NULLfor migrated cities (distinguishes from Google imports) - Idempotent migration (skips existing cities)
Source: backend/database/migrations/2025_12_30_074906_migrate_airports_cities_to_pois.php
Integration with Itineraries
Section titled “Integration with Itineraries”The itinerary system uses POIs for all location references. See Product Templates for the complete schema format.
Arrival Item
Section titled “Arrival Item”The first itinerary item uses arrival_poi_id to reference the entry point POI.
Day Items
Section titled “Day Items”Day items use the locations array containing POI IDs for all locations visited that day. Routes include from_id and to_id fields referencing POIs.
PoiMultiSelect Component
Section titled “PoiMultiSelect Component”For selecting multiple POIs in a day (multi-segment routes):
use App\Filament\Components\Fields\PoiMultiSelect;
PoiMultiSelect::make('locations')(new PoiMultiSelect())->types([PoiType::City])->build('locations')Features:
- Hybrid search: local database + Google Places API fallback
- Auto-creates POIs from Google Places when selected
- Returns array of POI IDs
Source: backend/app/Filament/Components/Fields/PoiMultiSelect.php
PoiMultiSelect Helper Methods
Section titled “PoiMultiSelect Helper Methods”// Resolve POI IDs to namesPoiMultiSelect::resolveNames([1, 2, 3]); // ["Nairobi", "Amboseli", "Mombasa"]PoiMultiSelect::resolveName(123); // "Nairobi"Provider Deduplication
Section titled “Provider Deduplication”POIs from external providers (Google Places) use a partial unique index:
CREATE UNIQUE INDEX pois_provider_provider_id_uniqueON pois (provider, provider_id)WHERE provider IS NOT NULLThis prevents duplicate imports while allowing multiple POIs with provider = NULL.
Related
Section titled “Related”- Google Places Integration - Import POIs from Google
- Product Templates - Itinerary structure
- Products Admin - Product management