Design: Place Search & Geocoding for Location Entry¶
Date: 2025-11-20 Author: Jake Celentano (via Claude Code) Status: Proposal Project: trip-planner
Problem Statement¶
Currently, locations in the trip-planner database have no coordinates. When examining the Italy 2026 trip:
- Locations exist as placeholders ("LOC1", "test")
- latitude and longitude fields are NULL
- address field is empty
- Map displays nothing because there are no valid coordinates
Root cause: No UX for entering location data. Users would need to manually type latitude/longitude coordinates, which is: - Unusable (who knows the exact coordinates of the Duomo in Florence?) - Error-prone (wrong decimal places, lat/lng swapped) - Time-consuming (looking up coordinates separately)
Goals¶
- [ ] Users can search for places by name (e.g., "Duomo Florence")
- [ ] Autocomplete suggestions appear as user types
- [ ] Selecting a place auto-fills: name, address, latitude, longitude
- [ ] Locations appear on map immediately after adding
- [ ] Works for cities, landmarks, restaurants, hotels, addresses worldwide
- [ ] Handles location types (accommodation, restaurant, attraction, transport)
Non-Goals¶
- Drawing custom routes on map (future - use Mapbox Directions API)
- Importing locations from Google Maps/TripAdvisor (future)
- Offline place search (requires internet for geocoding)
- Multi-language place names (Mapbox handles this automatically)
- Custom POI database (rely on Mapbox data)
Proposed Solution¶
High-Level Architecture¶
┌─────────────────────────────────────────┐
│ Trip Detail Page │
│ ├─ Itinerary Tabs (Day 1, Day 2...) │
│ └─ TripMap (existing) │
└───────────────┬─────────────────────────┘
│
v
┌─────────────────────────────────────────┐
│ AddLocationDialog (NEW) │
│ ├─ PlaceSearchInput (NEW) │
│ │ └─ Mapbox Geocoding API │
│ ├─ Location Type Selector │
│ ├─ Visit Time Input │
│ └─ Notes Field │
└───────────────┬─────────────────────────┘
│
v
POST /api/locations
│
v
Insert to Supabase
│
v
Update Map + Itinerary UI
Core Components¶
1. PlaceSearchInput (components/map/PlaceSearchInput.tsx)¶
Responsibility: Autocomplete search for places using Mapbox Geocoding API
Tech: Client Component with debounced search
Key features: - Text input with autocomplete dropdown - Debounced API calls (300ms delay to avoid excessive requests) - Display suggestions with formatted address - Click suggestion → populate parent form - Loading state while searching - Error handling for API failures
API Used: Mapbox Geocoding API
// Example API call
GET https://api.mapbox.com/geocoding/v5/mapbox.places/duomo%20florence.json
?access_token=<TOKEN>
&types=poi,address
&limit=5
Response structure:
{
"features": [
{
"place_name": "Cathedral of Santa Maria del Fiore, Florence, Italy",
"center": [11.2560, 43.7731], // [lng, lat]
"properties": {
"category": "landmark, place of worship"
}
}
]
}
2. AddLocationDialog (components/trips/AddLocationDialog.tsx)¶
Responsibility: Form to add location to specific itinerary day
Tech: Client Component with form state
Dependencies: PlaceSearchInput, location type selector
Form fields: - Place search (uses PlaceSearchInput) - Location type: dropdown (accommodation/restaurant/attraction/transport/other) - Visit time: time picker (optional) - Duration: number input in minutes (optional) - Notes: textarea (optional)
Validation: - Name required - Latitude/longitude required (auto-filled from search) - Location type required - Visit time optional but recommended
Submit flow:
1. User searches for place → lat/lng auto-filled
2. User selects location type
3. User optionally sets visit time
4. Click "Add Location" → POST to /api/locations
5. On success → close dialog, refresh itinerary
6. Location appears on map immediately
3. API Route: Create Location (/app/api/locations/route.ts)¶
Responsibility: Create location in database
Tech: Next.js API route with Supabase
Endpoint:
POST /api/locations
{
itinerary_id: string // UUID
name: string // "Cathedral of Santa Maria del Fiore"
address: string // "Piazza del Duomo, 50122 Florence, Italy"
latitude: number // 43.7731
longitude: number // 11.2560
location_type: string // "attraction"
visit_time?: string // "09:00" (HH:MM format)
duration_minutes?: number
notes?: string
}
Response:
{
id: string,
...location data
}
Validation: - Verify itinerary exists and belongs to current user (RLS check) - Ensure latitude is between -90 and 90 - Ensure longitude is between -180 and 180 - Validate location_type is one of enum values - Auto-increment sort_order within itinerary
4. Enhanced Trip Detail Page (/app/trips/[id]/page.tsx)¶
Additions: - "Add Location" button for each day's itinerary - Opens AddLocationDialog with pre-selected itinerary_id - After location added → refetch itinerary data - TripMap automatically updates (already wired to itineraries prop)
Data Flow¶
User types "Duomo"
↓
PlaceSearchInput debounces (300ms)
↓
Call Mapbox Geocoding API
↓
Display suggestions dropdown
↓
User selects "Cathedral of Santa Maria del Fiore"
↓
Form auto-fills:
- name: "Cathedral of Santa Maria del Fiore"
- address: "Piazza del Duomo, 50122 Florence, Italy"
- latitude: 43.7731
- longitude: 11.2560
↓
User selects type: "attraction"
User sets visit time: "09:00"
↓
User clicks "Add Location"
↓
POST /api/locations
↓
Insert into trip_planner.locations table
↓
Return success + location data
↓
Dialog closes, itinerary refetches
↓
Map displays new marker at coordinates
Mapbox Geocoding Integration¶
API Details:
- Endpoint:
https://api.mapbox.com/geocoding/v5/mapbox.places/{query}.json - Authentication: Access token in query params or header
- Rate limits: 100,000 requests/month (free tier)
- Pricing: $0.50 per 1,000 requests after free tier
Request parameters:
query: Search text (URL-encoded)
access_token: NEXT_PUBLIC_MAPBOX_TOKEN
types: poi,address (points of interest + addresses)
limit: 5 (max suggestions)
language: en (can support multiple languages later)
proximity: lng,lat (optional - bias results near user location)
Response parsing:
interface GeocodingFeature {
id: string
place_name: string // Full formatted address
center: [number, number] // [lng, lat]
place_type: string[] // ["poi", "address"]
properties: {
category?: string // "restaurant, italian"
}
context?: Array<{ // Hierarchical context
id: string
text: string // "Florence", "Italy"
}>
}
Best practices: - Cache recent searches (optional optimization) - Debounce to avoid excessive API calls - Show "No results" state gracefully - Handle network errors with retry logic - Show user-friendly error messages
Alternatives Considered¶
Option 1: Google Places API¶
Pros: - More comprehensive POI data - Better restaurant/business info - Reviews/ratings available
Cons: - Requires separate API key/billing - More complex integration - Already using Mapbox for map rendering - Overkill for basic geocoding
Verdict: Rejected - stick with Mapbox for consistency
Option 2: Manual Lat/Lng Entry¶
Pros: - No API dependency - Simple implementation - No rate limits
Cons: - Unusable UX - Error-prone - Requires external lookups - Defeats purpose of intuitive trip planner
Verdict: Rejected - this is the problem we're solving
Option 3: OpenStreetMap Nominatim¶
Pros: - Free and open source - No API key required - Good coverage
Cons: - Lower quality results than Mapbox - Slower response times - Rate limited (1 req/sec) - Already paying for Mapbox
Verdict: Rejected - Mapbox is better quality and we're already using it
Option 4: Mapbox Geocoding API ← Chosen¶
Pros: - Already using Mapbox for map rendering - Single API key/billing - Excellent global coverage - Fast autocomplete responses - Clean API design - 100k free requests/month
Cons: - Requires API key - Costs after free tier - Internet dependency
Verdict: Accepted - best integration with existing stack
Implementation Plan¶
Phase 1: PlaceSearchInput Component (1.5h)¶
- [ ] Create
components/map/PlaceSearchInput.tsx(45 min) - Text input with controlled state
- Debounced onChange handler
- Fetch from Mapbox Geocoding API
- Display suggestions dropdown
- [ ] Style suggestions dropdown (20 min)
- Tailwind CSS styling
- Loading spinner
- Hover states
- [ ] Handle selection callback (15 min)
- Emit selected place to parent
- Clear suggestions on select
- [ ] Error handling (10 min)
- Network errors
- No results state
- Invalid API key
Phase 2: AddLocationDialog Component (1h)¶
- [ ] Create
components/trips/AddLocationDialog.tsx(30 min) - Modal/dialog with form
- Integrate PlaceSearchInput
- Location type dropdown
- Visit time picker
- Notes textarea
- [ ] Form state management (15 min)
- Track all fields
- Auto-fill from place search
- Validation
- [ ] Submit handler (15 min)
- Call POST /api/locations
- Handle success/error
- Close dialog on success
Phase 3: Location API Route (45 min)¶
- [ ] Create
/app/api/locations/route.ts(30 min) - POST handler
- Validate request body
- Insert into Supabase
- Return created location
- [ ] Test with curl/Postman (15 min)
- Valid requests
- Missing required fields
- Invalid coordinates
Phase 4: Integration with Trip Detail Page (1h)¶
- [ ] Update
/app/trips/[id]/page.tsx(20 min) - Add "Add Location" button per itinerary day
- Wire up AddLocationDialog
- Pass itinerary_id to dialog
- [ ] Refetch logic after location added (15 min)
- Refresh itinerary data
- Verify map updates automatically
- [ ] Loading/error states (15 min)
- Show loading during API call
- Display error messages
- [ ] Polish UI (10 min)
- Button placement
- Dialog styling
- Responsive layout
Phase 5: Testing & Refinement (45 min)¶
- [ ] Manual testing workflow (20 min)
- Search for various places (cities, landmarks, restaurants)
- Verify coordinates are correct
- Check map displays markers
- Test edge cases (no results, network errors)
- [ ] Fix bugs found during testing (20 min)
- [ ] Documentation (5 min)
- Add usage notes to README
- Document Mapbox API key setup
Total estimate: ~5 hours
Risks & Mitigations¶
| Risk | Impact | Probability | Mitigation |
|---|---|---|---|
| Mapbox API key not set | High | Medium | Check env var, show clear error message |
| Rate limit exceeded | Medium | Low | Monitor usage, add caching if needed |
| Poor search results | Medium | Medium | Allow manual coordinate override |
| Network timeout | Medium | Low | Add 5s timeout, show retry button |
| Coordinates saved incorrectly | High | Low | Validate lat/lng ranges before insert |
| RLS blocks location insert | High | Medium | Verify itinerary belongs to user via join |
Testing Strategy¶
Manual testing (current standard):
Happy path: 1. Navigate to trip detail page 2. Click "Add Location" for Day 1 3. Search "Duomo Florence" 4. Select "Cathedral of Santa Maria del Fiore" 5. Verify form auto-fills coordinates 6. Select location type "attraction" 7. Set visit time "09:00" 8. Click submit 9. Verify location appears in day's itinerary 10. Verify marker appears on map at correct position
Edge cases: - Search for nonexistent place → verify "No results" message - Submit without selecting place → verify validation error - Network error during search → verify error message - Invalid Mapbox token → verify clear error to user - Search international locations (Tokyo, Paris) → verify works globally
Database verification:
SELECT name, latitude, longitude, address
FROM trip_planner.locations
WHERE itinerary_id = '<test-itinerary-id>';
Future (when testing added): - Unit tests for PlaceSearchInput debouncing - Integration tests for API route - Component tests for AddLocationDialog
Observability¶
Not critical for MVP - standard logging sufficient.
Monitor: - Console errors for failed Mapbox API calls - Check Supabase logs for INSERT errors - Monitor Mapbox API usage dashboard (avoid surprise bills)
Future metrics: - Average searches per location added - Most common search queries - Geocoding API response times
Security Considerations¶
Mapbox API Key:
- Use NEXT_PUBLIC_MAPBOX_TOKEN (safe to expose - domain-restricted)
- Configure URL restrictions in Mapbox dashboard to limit to:
- http://10.89.97.163:3000 (dev)
- http://travel.internal (prod)
- Domain whitelist prevents abuse
Input validation: - Sanitize search queries before sending to Mapbox - Validate coordinates on backend (not just frontend) - Ensure latitude ∈ [-90, 90], longitude ∈ [-180, 180] - RLS ensures users can only add locations to their own trips
Rate limiting: - No additional rate limiting needed (Mapbox handles this) - Monitor usage to avoid bill shock
Rollout Plan¶
- Development (5 hours)
- Implement all phases in
/root/projects/trip-planner -
Test with Italy 2026 trip
-
Testing (30 min)
- Add real locations to Italy trip
- Verify map displays correctly
-
Test various place types and locations
-
Commit (5 min)
- Git commit with message:
feat: add place search and geocoding for location entry -
Push to GitHub
-
Documentation (10 min)
- Update README with Mapbox token setup
-
Add screenshots of place search in action
-
Production deployment (future)
- Update ConfigMap with prod Mapbox token
- Deploy to k8s when ready
Success Metrics¶
Immediate (MVP): - [ ] Can search for "Duomo Florence" and get relevant results - [ ] Selecting a place fills in name, address, lat, lng - [ ] Added location appears on map with correct marker position - [ ] Italy 2026 trip shows real locations instead of empty map - [ ] Can add 10+ locations without issues
Long-term: - Majority of locations added via search (not manual coordinates) - <1% invalid coordinates in database - <5 seconds average time to add a location - No user reports of "can't find my place"
Open Questions¶
- [x] Use Mapbox or Google Places API? → Mapbox (already integrated)
- [ ] Allow manual coordinate override? → YES (add optional lat/lng fields for edge cases)
- [ ] Support reverse geocoding (pick from map)? → Future enhancement
- [ ] Cache search results? → No for MVP, add if performance issues
- [ ] Filter by place type during search? → No, show all results
- [ ] Proximity bias (search near previous locations)? → Future enhancement
- [ ] Multi-language support? → Future (Mapbox supports it, just pass
languageparam)
Future Enhancements¶
Phase 2 features (not in this PR):
- Reverse Geocoding - Click map to add location
- User clicks map → get coordinates
- Call Mapbox reverse geocoding → get place name
-
Pre-fill form with place data
-
Route Visualization - Show actual roads between locations
- Use Mapbox Directions API
- Display estimated travel time/distance
-
Support walking, driving, transit modes
-
Place Details - Show photos, ratings, hours
- Integrate with additional APIs
- Show preview in autocomplete
-
Help users choose best option
-
Offline Support - Cache geocoding results
- Store recent searches in IndexedDB
- Allow offline editing of existing locations
-
Sync when back online
-
Bulk Import - Import from CSV, Google Maps, TripAdvisor
- Upload trip itinerary CSV
- Parse and geocode all locations
-
Review before importing
-
Smart Suggestions - AI-powered location recommendations
- "Also add nearby restaurants"
- "Optimize visit order by proximity"
- "Top attractions you might have missed"
Status: Ready for implementation Next Step: Phase 1 - PlaceSearchInput Component Estimated completion: 2025-11-20 (1 day dev time)