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:

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

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
airport_id bigint FK to airports (nullable, nullOnDelete, indexed). Links a POI to an airport so getDisplayName() can append its IATA code
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, backend/database/migrations/2026_03_03_093539_add_airport_id_to_pois_table.php

Per-locale overrides of a POI’s name, used by user-facing surfaces that render city/airport names (flight text on the PDP, itinerary stop labels, country/region stop lists) so a Spanish PBM shows “Ciudad del Cabo” rather than the raw OpenFlights “Cape Town”.

Table: poi_translations

Column Type Purpose
poi_id bigint FK to pois (cascadeOnDelete)
locale varchar(12) Locale code
name varchar Localized POI name

Constraint: Unique (poi_id, locale).

Poi::translations() is a HasMany to PoiTranslation. Poi::nameFor(?string $locale) returns the translated name for the requested locale, falling back to the raw pois.name when no row exists (or when $locale is null/empty).

Source: backend/database/migrations/2026_05_26_144446_create_poi_translations_table.php, backend/app/Models/PoiTranslation.php, backend/app/Models/Poi.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 (scopeActive, scopeOfType, scopeCities, scopeAttractions)

$poi->getDisplayName(); // "Barcelona (Spain)", appends " - BCN" when linked to an airport
$poi->nameFor('es_ES'); // Localized name, falls back to name when no translation exists
$poi->getImageUrl(); // Full URL to image or null
$poi->isFromProvider(); // true if imported from external source

Source: backend/app/Models/Poi.php (getDisplayName, nameFor, getImageUrl, isFromProvider)

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.