Skip to content

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.

  • 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

POIs are classified by the PoiType enum:

TypeValueIconUse Case
Citycitybuilding-office-2Metropolitan areas, tour stops
AttractionattractionstarMuseums, theme parks, tourist sites
Landmarklandmarkmap-pinChurches, monuments, historic buildings
NaturalFeaturenatural_featureglobe-americasParks, mountains, natural wonders

Source: backend/app/Enums/PoiType.php

Table: pois

ColumnTypePurpose
namevarchar(200)Display name
typevarchar(50)PoiType enum value
providervarchar(50)Data source (e.g., google_places)
provider_idvarchar(255)External ID for deduplication
countryvarchar(100)Full country name
country_codechar(2)ISO country code
regionvarchar(100)Region/state within the country (nullable)
latitudedecimal(10,8)Geographic latitude
longitudedecimal(11,8)Geographic longitude
descriptiontextEditorial description
image_pathvarchar(500)Storage path for image
raw_provider_datajsonbOriginal provider response
statusbooleanActive/inactive flag
sort_orderintDisplay ordering
ColumnsPurpose
(type, status)Filter by type and active status
(country_code, status)Filter by country
nameName search
(provider, provider_id) WHERE provider IS NOT NULLUnique provider constraint

Source: backend/database/migrations/2025_12_30_074755_create_pois_table.php

// Get only active POIs
Poi::active()->get();
// Filter by type
Poi::ofType(PoiType::City)->get();
Poi::ofType('landmark')->get();
// Convenience scopes
Poi::cities()->get();
Poi::attractions()->get();

Source: backend/app/Models/Poi.php:77-116

$poi->getDisplayName(); // "Barcelona (Spain)" or "Barcelona"
$poi->getImageUrl(); // Full URL to image or null
$poi->isFromProvider(); // true if imported from external source

Source: backend/app/Models/Poi.php:121-148

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 types
PoiSelect::types([PoiType::City, PoiType::Attraction])->select('poi_id')
// Cities only
PoiSelect::types([PoiType::City])->select('destination_id')
// Get POI name for display
PoiSelect::getPoiName($poiId); // "Barcelona"
PoiSelect::getDisplayName($poiId); // "Barcelona (Spain)"
// Clear cached options
PoiSelect::clearCache();

Source: backend/app/Filament/Components/Fields/PoiSelect.php

POI options are cached for 1 hour (3600 seconds) with cache keys:

  • poi_select_options_all - All active POIs
  • poi_select_options_city - Cities only
  • poi_select_options_city_attraction - Cities and attractions

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

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 = NULL for 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

The itinerary system uses POIs for all location references. See Product Templates for the complete schema format.

The first itinerary item uses arrival_poi_id to reference the entry point POI.

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.

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

// Resolve POI IDs to names
PoiMultiSelect::resolveNames([1, 2, 3]); // ["Nairobi", "Amboseli", "Mombasa"]
PoiMultiSelect::resolveName(123); // "Nairobi"

POIs from external providers (Google Places) use a partial unique index:

CREATE UNIQUE INDEX pois_provider_provider_id_unique
ON pois (provider, provider_id)
WHERE provider IS NOT NULL

This prevents duplicate imports while allowing multiple POIs with provider = NULL.