# Project export: Marauder

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Navigate smarter with semantic search for maps — just say what you want, like “go to the airport and get gas with <5 min detour,” and we’ll find the optimal route. No filters, just intent.
- Devpost: https://devpost.com/software/marauder-6esu5t
- GitHub: https://github.com/Vedaant-J/CalHacksMaps
- Team: 3 GitHub contributor(s) — vedaant jain (3 commits), cheung-arthur (3 commits), Vinayak Sharma (1 commits)

## Devpost submission (written by the team)

### Inspiration

We were in the car and the driver wanted to a route with specific constraints but couldn't easily do it while driving.

### What it does

It uses semantic search for finding the optimal route given constraints, making it very easy to navigate.

### How we built it

We used React to make the web interface and Cursor to help with the semantic search.

### Challenges we ran into

We had a lot of bugs since we were vibe coding. Had to use our technical expertise to debug functionality features.

### Accomplishments we're proud of

We were able to complete our project in 24 hours and make it user-friendly.

### What we learned

How to ideate and build a product in less than 24 hours.

### What's next

Making it better, faster, more accurate, and scale it.

## README (from the GitHub repository)

# Semantic Maps Assistant

A full-stack web application that overlays semantic, conversational search onto Google Maps. Users can plan routes and use natural language to find points of interest along their journey.

## Architecture

- **Backend**: Python FastAPI with Google Gemini Pro + Places API
- **Frontend**: React.js with Google Maps integration
- **AI**: Semantic query processing using Gemini Pro
- **APIs**: Google Maps, Places, and Directions services

## Quick Start

### Prerequisites

- Python 3.8+
- Node.js 16+
- **Two separate Google API keys**:
  - **Google Cloud API Key** (for Maps, Places, Directions APIs)
  - **Gemini API Key** (for AI semantic processing)

### Setup

1. **Clone and navigate to project**:
   ```bash
   git clone <repository>
   cd CalHacks1
   ```

2. **Backend Setup**:
   ```bash
   cd server
   pip install -r requirements.txt
   
   # Create .env file with both API keys:
   cp env.example .env
   # Edit .env and add:
   # GOOGLE_API_KEY=your_google_cloud_api_key_here
   # GEMINI_API_KEY=your_gemini_api_key_here
   ```

3. **Frontend Setup**:
   ```bash
   cd ../semantic-maps-app
   npm install
   
   # Create .env file:
   cp env.example .env
   # Edit .env and add:
   # REACT_APP_GOOGLE_MAPS_API_KEY=your_google_cloud_api_key_here
   ```

4. **Start Both Services**:
   ```bash
   # From project root:
   ./start-dev.sh    # Unix/Mac
   # OR
   start-dev.bat     # Windows
   ```

   Or start manually:
   ```bash
   # Terminal 1 - Backend:
   cd server
   uvicorn main:app --reload
   
   # Terminal 2 - Frontend:
   cd semantic-maps-app  
   npm start
   ```

### API Keys Setup

#### Google Cloud API Key
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create/select a project
3. Enable these APIs:
   - Maps JavaScript API
   - Places API
   - Directions API
4. Create credentials → API Key
5. Use this key for both `GOOGLE_API_KEY` (backend) and `REACT_APP_GOOGLE_MAPS_API_KEY` (frontend)

#### Gemini API Key  
1. Go to [Google AI Studio](https://aistudio.google.com/)
2. Create API key
3. Use this key for `GEMINI_API_KEY` (backend only)

## Usage

1. **Open the app**: http://localhost:3000
2. **Plan a route**: Enter origin and destination
3. **Semantic search**: Ask questions like:
   - "Find coffee shops along the way"
   - "Good restaurants for lunch"
   - "Gas stations near the highway"
4. **View results**: See places marked on the map
5. **Add to route**: Click markers to add stops

## API Endpoints

### `POST /api/find-places-on-route`

**Request**:
```json
{
  "query": "coffee shops along the route",
  "route": {
    "routes": [/* Google DirectionsResult object */]
  }
}
```

**Response**:
```json
[
  {
    "place_id": "ChIJ...",
    "name": "Blue Bottle Coffee",
    "geometry": {
      "location": {"lat": 37.7749, "lng": -122.4194}
    },
    "rating": 4.5
  }
]
```

## Features

- ✅ **Semantic Search**: Natural language queries powered by Gemini Pro
- ✅ **Route Planning**: Google Maps integration with directions
- ✅ **Smart Filtering**: AI converts queries to relevant place searches  
- ✅ **Interactive Map**: Click markers to add stops to your route
- ✅ **Responsive Design**: Works on desktop and mobile

## Architecture Details

### Backend (FastAPI)
- **Semantic Processing**: Gemini Pro converts natural language to structured queries
- **Places Search**: Google Places API finds relevant locations
- **Route Analysis**: Calculates midpoints and search areas
- **API Integration**: Clean REST interface for frontend

### Frontend (React)
- **Maps Integration**: `@vis.gl/react-google-maps` for modern React integration
- **State Management**: React hooks for route and places data
- **UI Components**: Clean, responsive interface
- **Real-time Updates**: Dynamic map updates as user interacts

## Troubleshooting

### Common Issues

1. **"Could not import module 'main'"**: Run uvicorn from the `server/` directory
2. **API Key errors**: Ensure both API keys are properly configured in .env files
3. **CORS errors**: Backend runs on :8000, frontend on :3000 - CORS is configured
4. **Maps not loading**: Check `REACT_APP_GOOGLE_MAPS_API_KEY` in frontend .env

### Development Tips

- **Backend logs**: Check terminal running uvicorn for detailed error messages
- **Frontend debugging**: Open browser DevTools for React errors
- **API testing**: Use curl or Postman to test backend endpoints directly
- **Environment**: Ensure .env files are not committed to version control

## Future Enhancements

- [ ] **Multi-stop optimization**: Optimize route order for multiple stops
- [ ] **User preferences**: Remember favorite place types and locations  
- [ ] **Social features**: Share routes and recommendations
- [ ] **Offline mode**: Cache routes and places for offline use
- [ ] **Advanced filters**: Price range, ratings, hours, etc.
- [ ] **Voice interface**: Voice commands for hands-free operation

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request

## License

MIT License - see LICENSE file for details. 

## Detected evidence (automated analysis)

Indexed codebase: 18 recognized source files, 126 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (26 of 26)

```
.gitignore
README.md
semantic-maps-app/.eslintrc.js
semantic-maps-app/.gitignore
semantic-maps-app/package.json
semantic-maps-app/public/index.html
semantic-maps-app/public/manifest.json
semantic-maps-app/public/robots.txt
semantic-maps-app/README.md
semantic-maps-app/src/App.css
semantic-maps-app/src/App.js
semantic-maps-app/src/App.test.js
semantic-maps-app/src/index.css
semantic-maps-app/src/index.js
semantic-maps-app/src/PlaceAutocomplete.js
semantic-maps-app/src/reportWebVitals.js
semantic-maps-app/src/setupTests.js
semantic-maps-app/src/VoiceInput.js
server/main.py
server/requirements-test.txt
server/requirements.txt
server/run_tests.py
server/test_integration.py
server/test_main.py
start-dev.bat
start-dev.sh
```

### Dependencies

- semantic-maps-app/package.json: @testing-library/dom@^10.4.0, @testing-library/jest-dom@^6.6.3, @testing-library/react@^16.3.0, @testing-library/user-event@^13.5.0, @vis.gl/react-google-maps@^1.5.3, react@^19.1.0, react-dom@^19.1.0, react-scripts@5.0.1, web-vitals@^2.1.4
- server/requirements.txt: fastapi, google-generativeai, googlemaps, pydantic, python-dotenv, uvicorn[standard]

### Recent commits (newest first)

- initial
- initial
- veedant is handsome
- added voice feature to new UI
- peen gobbler
- Update UI to ChatGPT-style design with unified prompt window and route inputs
- inital_commit

## Key source files (fetched from GitHub, selected and truncated for size)

### server/requirements.txt

```
fastapi
uvicorn[standard]
python-dotenv
google-generativeai
googlemaps
pydantic 
```

### semantic-maps-app/package.json

```
{
  "name": "semantic-maps-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.3.0",
    "@testing-library/user-event": "^13.5.0",
    "@vis.gl/react-google-maps": "^1.5.3",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

```

### semantic-maps-app/src/index.js

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

```

### server/main.py

```python
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Dict, Any, List, Tuple
import os
import json
from dotenv import load_dotenv
import math

# Optional imports, wrapped in try/except so devs without keys can still run the server
try:
    import google.generativeai as genai
    import googlemaps
except ImportError:
    genai = None  # type: ignore
    googlemaps = None  # type: ignore

load_dotenv()

GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")  # For Google Maps/Places API
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")  # For Gemini AI

app = FastAPI(
    title="Semantic Maps Assistant API",
    version="1.0.0",
)

# CORS configuration – during development the CRA dev server runs on port 3000
origins = [
    "http://localhost:3000",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class RouteQuery(BaseModel):
    """Expected payload from the React frontend."""

    query: str  # Natural-language search string from the user
    route: Dict[str, Any]  # Google DirectionsResult object (JSON-serialised)


class VoiceQuery(BaseModel):
    """Expected payload for parsing voice commands."""
    command: str


def determine_search_location(query: str, start_pos: tuple, end_pos: tuple, mid_pos: tuple) -> tuple:
    """Determine where to search based on query constraints."""
    query_lower = query.lower()
    
    # Check for location-specific keywords
    if any(word in query_lower for word in ['near destination', 'at destination', 'destination area', 'end of trip']):
        return end_pos
    elif any(word in query_lower for word in ['near start', 'at start', 'beginning', 'start of trip', 'departure']):
        return start_pos
    else:
        # Default to midpoint for "along the route" searches
        return mid_pos


def haversine_distance(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
    """Calculate the great circle distance between two points on earth (in meters)."""
    R = 6371000  # Earth's radius in meters
    
    lat1_rad = math.radians(lat1)
    lat2_rad = math.radians(lat2)
    delta_lat = math.radians(lat2 - lat1)
    delta_lng = math.radians(lng2 - lng1)
    
    a = (math.sin(delta_lat / 2) ** 2 + 
         math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(delta_lng / 2) ** 2)
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    
    return R * c


def extract_route_points(route_data: Dict[str, Any]) -> List[Tuple[float, float]]:
    """Extract coordinate points from route geometry for proximity calculations."""
    points = []
    
    try:
        routes = route_data.get("routes", [])
        if not routes:
            return points
            
        route = routes[0]
        legs = route.get("legs", [])
        
        for leg in legs:
            steps = leg.get("steps", [])
            for step in steps:
                # Add start point of each step
                start_loc = step.get("start_location", {})
                if start_loc:
                    points.append((start_loc.get("lat"), start_loc.get("lng")))
                
                # Add end point of each step  
                end_loc = step.get("end_location", {})
                if end_loc:
                    points.append((end_loc.get("lat"), end_loc.get("lng")))
    
    except Exception as e:
        print(f"Error extracting route points: {e}")
    
    return points


def min_distance_to_route(place_lat: float, place_lng: float, route_points: List[Tuple[float, float]]) -> float:
    """Calculate minimum distance from a place to any point on the route."""
    if not route_points:
        return float('inf')
    
    min_dist = float('inf')
    for route_lat, route_lng in route_points:
        if route_lat is not None and route_lng is not None:
            dist = haversine_distance(place_lat, place_lng, route_lat, route_lng)
            min_dist = min(min_dist, dist)
    
    return min_dist


def calculate_recommendation_score(place: Dict[str, Any], query: str, route_distance: float) -> float:
    """Calculate a comprehensive recommendation score based on multiple factors."""
    score = 0.0
    
    # Rating factor (0-50 points)
    rating = place.get("rating", 0)
    if rating > 0:
        score += (rating / 5.0) * 50
    
    # Review count factor (0-20 points) - logarithmic scale
    review_count = place.get("user_ratings_total", 0)
    if review_count > 0:
        # Use log scale: 1-10 reviews = 5pts, 10-100 = 10pts, 100-1000 = 15pts, 1000+ = 20pts
        score += min(20, math.log10(review_count + 1) * 7)
    
    # Proximity factor (0-20 points) - closer is better
    if route_distance < float('inf'):
        # Within 500m = 20pts, 1km = 15pts, 2km = 10pts, 5km = 5pts, 10km+ = 0pts
        if route_distance <= 500:
            score += 20
        elif route_distance <= 1000:
            score += 15
        elif route_distance <= 2000:
            score += 10
        elif route_distance <= 5000:
            score += 5
    
    # Price level factor (0-10 points) - moderate pricing preferred
    price_level = place.get("price_level")
    if price_level is not None:
        # Prefer moderate pricing: free=5pts, inexpensive=8pts, moderate=10pts, expensive=7pts, very_expensive=4pts
        price_scores = {0: 5, 1: 8, 2: 10, 3: 7, 4: 4}
        score += price_scores.get(price_level, 0)
    
    return score


def get_gemini_recommendations_from_raw_data(places: List[Dict], query: str, route_points: List[Tuple[float, float]]) -> Dict[str, Any]:
    """Let Gemini analyze raw place data and make recommendations without pre-sorting."""
    try:
        genai.configure(api_key=GEMINI_API_KEY)
        gemini = genai.GenerativeModel("gemini-1.5-pro")
        
        print(f"DEBUG: Sending {len(places)} places to Gemini for analysis in original order")
        print(f"DEBUG: Route has {l
[truncated — 35589 more characters]
```

### semantic-maps-app/src/App.js

```javascript
/* eslint-disable no-undef */
import React, { useState } from 'react';
import { APIProvider, Map, Marker, InfoWindow, useMap } from '@vis.gl/react-google-maps';
import PlaceAutocomplete from './PlaceAutocomplete';
import VoiceInput from './VoiceInput';
import './App.css';

// Get API key from environment variable
const GOOGLE_MAPS_API_KEY = process.env.REACT_APP_GOOGLE_MAPS_API_KEY || 'YOUR_FRONTEND_API_KEY_HERE';

// Debug: Log the API key being used (first 10 chars for security)
console.log('Google Maps API Key (first 10 chars):', GOOGLE_MAPS_API_KEY ? GOOGLE_MAPS_API_KEY.substring(0, 10) + '...' : 'NOT FOUND');
console.log('Environment variable REACT_APP_GOOGLE_MAPS_API_KEY:', process.env.REACT_APP_GOOGLE_MAPS_API_KEY ? 'FOUND' : 'NOT FOUND');

function App() {
  const [origin, setOrigin] = useState('');
  const [destination, setDestination] = useState('');
  const [semanticQuery, setSemanticQuery] = useState('');
  const [mapCenter] = useState({ lat: 37.7749, lng: -122.4194 }); // San Francisco default
  const [suggestedPlaces, setSuggestedPlaces] = useState([]);
  const [recommendedPlaces, setRecommendedPlaces] = useState([]);
  const [selectedPlace, setSelectedPlace] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [isGenerating, setIsGenerating] = useState(false);
  const [error, setError] = useState('');
  const [currentRoute, setCurrentRoute] = useState(null);
  const [routeStartEnd, setRouteStartEnd] = useState(null);
  const [waypoints, setWaypoints] = useState([]);
  const [shouldPlanRoute, setShouldPlanRoute] = useState(false);
  const [isPromptSubmitted, setIsPromptSubmitted] = useState(false);
  const [pendingSemanticQuery, setPendingSemanticQuery] = useState('');

  // Handle delayed semantic search when route becomes available
  React.useEffect(() => {
    if (currentRoute && pendingSemanticQuery && isGenerating) {
      console.log('Route now available, executing pending semantic search...');
      handleSemanticSearch(currentRoute, pendingSemanticQuery).then(() => {
        setPendingSemanticQuery('');
        setIsGenerating(false);
      }).catch((error) => {
        console.error('Error in delayed semantic search:', error);
        setError('Failed to search for places. Please try again.');
        setPendingSemanticQuery('');
        setIsGenerating(false);
      });
    }
  }, [currentRoute, pendingSemanticQuery, isGenerating]);

  const handleSubmit = async () => {
    if (!origin.trim() || !destination.trim()) {
      setError('Please enter both origin and destination');
      return;
    }

    setIsPromptSubmitted(true);
    setIsLoading(true);
    setError('');
    
    try {
      // First, plan the route
      setShouldPlanRoute(true);
      
      // If we have constraints, start the search process
      if (semanticQuery.trim()) {
        setIsGenerating(true);
        
        // Wait for route with timeout, but don't fail if it takes too long
        let routeAvailable = false;
        let attempts = 0;
        const maxAttempts = 30; // 3 seconds
        
        while (!routeAvailable && attempts < maxAttempts) {
          await new Promise(resolve => setTimeout(resolve, 100));
          routeAvailable = currentRoute !== null;
          attempts++;
        }
        
                 if (routeAvailable) {
           console.log('Route ready, starting semantic search...');
           await handleSemanticSearch(currentRoute, semanticQuery);
           setIsGenerating(false);
         } else {
           console.log('Route still calculating, will search when ready...');
           // Set a flag to search when route becomes available - don't clear isGenerating yet
           setPendingSemanticQuery(semanticQuery);
         }
      }
      
    } catch (err) {
      console.error('Error in handleSubmit:', err);
      setError('Failed to process request. Please try again.');
      setIsGenerating(false);
    } finally {
      setIsLoading(false);
    }
  };

  const handleSemanticSearch = async (routeData, query) => {
    if (!routeData || !query.trim()) {
      return;
    }

    try {
      const response = await fetch('http://localhost:8000/api/find-places-on-route', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query: query,
          route: routeData
        })
      });

      if (!response.ok) {
        throw new Error(`Server error: ${response.status}`);
      }

      const data = await response.json();
      console.log('Server response:', data);
      
      // Handle both old and new response formats
      if (Array.isArray(data)) {
        // Old format - just an array of places
        console.log('Using old format - array of places');
        setSuggestedPlaces(data);
        
        // Simple client-side recommendation: pick top 3 by rating
        const sortedByRating = [...data].sort((a, b) => (b.rating || 0) - (a.rating || 0));
        const topRecommendations = sortedByRating.slice(0, 3).map((place) => ({
          ...place,
          recommendation_reason: `High-rated choice with ${place.rating || 'N/A'} stars and ${place.user_ratings_total || 'many'} reviews`
        }));
        
        console.log('Client-side recommendations:', topRecommendations);
        setRecommendedPlaces(topRecommendations);
      } else {
        // New format - structured response
        console.log('Using new format - structured response');
        setSuggestedPlaces(data.all_places || []);
        setRecommendedPlaces(data.recommended_places || []);
      }
    } catch (err) {
      setError(`Failed to search places: ${err.message}`);
    }
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSubmit();
    }
  };

  const handleAddToRoute = (place) => {
    // Add the place as a waypoint
    const newWaypoint = {
      location: {
        lat: place.geometry.lo
[truncated — 23394 more characters]
```

### start-dev.sh

```shell
#!/bin/bash

# Semantic Maps Assistant - Development Startup Script
# This script starts both the backend and frontend servers

echo "🚀 Starting Semantic Maps Assistant..."

# Check if Python is available
if ! command -v python &> /dev/null; then
    echo "❌ Python is not installed. Please install Python 3.9+ and try again."
    exit 1
fi

# Check if Node.js is available
if ! command -v node &> /dev/null; then
    echo "❌ Node.js is not installed. Please install Node.js 14+ and try again."
    exit 1
fi

# Start backend server
echo "🐍 Starting Python FastAPI backend..."
cd server
if [ ! -d "venv" ]; then
    echo "Creating Python virtual environment..."
    python -m venv venv
fi

source venv/bin/activate
pip install -r requirements.txt

if [ ! -f ".env" ]; then
    echo "⚠️  No .env file found in server directory. Please copy env.example to .env and add your API key."
    exit 1
fi

uvicorn main:app --reload --port 8000 &
BACKEND_PID=$!
echo "✅ Backend started on http://localhost:8000 (PID: $BACKEND_PID)"

# Start frontend server
echo "⚛️  Starting React frontend..."
cd ../semantic-maps-app

if [ ! -d "node_modules" ]; then
    echo "Installing Node.js dependencies..."
    npm install
fi

if [ ! -f ".env" ]; then
    echo "⚠️  No .env file found in semantic-maps-app directory. Please copy env.example to .env and add your API key."
    kill $BACKEND_PID
    exit 1
fi

npm start &
FRONTEND_PID=$!
echo "✅ Frontend started on http://localhost:3000 (PID: $FRONTEND_PID)"

echo ""
echo "🎉 Both servers are running!"
echo "📱 Frontend: http://localhost:3000"
echo "🔧 Backend API: http://localhost:8000"
echo "📚 API Docs: http://localhost:8000/docs"
echo ""
echo "Press Ctrl+C to stop both servers"

# Function to cleanup on exit
cleanup() {
    echo ""
    echo "🛑 Stopping servers..."
    kill $BACKEND_PID 2>/dev/null
    kill $FRONTEND_PID 2>/dev/null
    echo "✅ Servers stopped"
    exit 0
}

# Set trap to cleanup on script exit
trap cleanup SIGINT SIGTERM

# Wait for both processes
wait $BACKEND_PID $FRONTEND_PID 
```

### semantic-maps-app/.eslintrc.js

```javascript
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: [
    'react',
  ],
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
  globals: {
    google: 'readonly',
    process: 'readonly',
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
}; 
```

### server/run_tests.py

```python
#!/usr/bin/env python3
"""
Test runner for Semantic Maps Assistant API
"""

import subprocess
import sys
import os

def run_tests():
    """Run all tests with proper configuration"""
    
    print("🧪 Running Semantic Maps Assistant API Tests")
    print("=" * 50)
    
    # Set test environment variables
    test_env = os.environ.copy()
    test_env.update({
        'GOOGLE_API_KEY': 'test_key_for_testing',
        'GEMINI_API_KEY': 'test_gemini_key_for_testing'
    })
    
    # Run basic tests
    print("\n🔍 Running Unit Tests")
    print("-" * 30)
    
    try:
        result = subprocess.run(
            ['python', '-m', 'pytest', 'test_main.py', '-v'],
            env=test_env,
            cwd=os.path.dirname(os.path.abspath(__file__))
        )
        
        if result.returncode == 0:
            print("✅ All tests PASSED")
            return True
        else:
            print("❌ Some tests FAILED")
            return False
            
    except Exception as e:
        print(f"💥 Error running tests: {e}")
        return False

if __name__ == "__main__":
    success = run_tests()
    sys.exit(0 if success else 1) 
```

### server/test_main.py

```python
import pytest
import json
import os
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from main import app, RouteQuery, determine_search_location, get_gemini_recommendations

# Test client for FastAPI
client = TestClient(app)

# Sample test data
SAMPLE_ROUTE = {
    "routes": [{
        "legs": [{
            "start_location": {"lat": 37.7749, "lng": -122.4194},
            "end_location": {"lat": 37.7849, "lng": -122.4094}
        }],
        "bounds": {
            "northeast": {"lat": 37.7849, "lng": -122.4094},
            "southwest": {"lat": 37.7749, "lng": -122.4194}
        }
    }]
}

SAMPLE_PLACE_RESPONSE = {
    "results": [
        {
            "place_id": "test_place_1",
            "name": "Test Restaurant",
            "geometry": {
                "location": {"lat": 37.7799, "lng": -122.4144}
            },
            "rating": 4.5,
            "user_ratings_total": 100,
            "types": ["restaurant", "food"]
        },
        {
            "place_id": "test_place_2", 
            "name": "Test Cafe",
            "geometry": {
                "location": {"lat": 37.7779, "lng": -122.4164}
            },
            "rating": 4.2,
            "user_ratings_total": 50,
            "types": ["cafe", "food"]
        }
    ]
}

SAMPLE_PLACE_DETAILS = {
    "result": {
        "place_id": "test_place_1",
        "name": "Test Restaurant",
        "geometry": {
            "location": {"lat": 37.7799, "lng": -122.4144}
        },
        "rating": 4.5,
        "user_ratings_total": 100,
        "price_level": 2,
        "formatted_address": "123 Test St, San Francisco, CA",
        "formatted_phone_number": "(555) 123-4567",
        "website": "https://testrestaurant.com",
        "opening_hours": {
            "weekday_text": ["Monday: 9:00 AM – 9:00 PM", "Tuesday: 9:00 AM – 9:00 PM"]
        },
        "photos": [{
            "photo_reference": "test_photo_ref"
        }],
        "types": ["restaurant", "food"]
    }
}

class TestHealthEndpoint:
    """Test the health check endpoint"""
    
    def test_health_check(self):
        """Test GET / returns correct health information"""
        response = client.get("/")
        
        assert response.status_code == 200
        data = response.json()
        
        assert data["message"] == "Semantic Maps Assistant API"
        assert data["version"] == "1.1"
        assert data["ai_service"] == "Google Gemini AI"
        assert data["maps_service"] == "Google Maps"


class TestSearchLocationDetermination:
    """Test the search location determination logic"""
    
    def test_destination_keywords(self):
        """Test queries with destination keywords"""
        start = (37.7749, -122.4194)
        end = (37.7849, -122.4094)
        mid = (37.7799, -122.4144)
        
        queries = [
            "restaurants near destination",
            "coffee shops at destination", 
            "hotels destination area",
            "parking end of trip"
        ]
        
        for query in queries:
            result = determine_search_location(query, start, end, mid)
            assert result == end, f"Query '{query}' should return destination location"
    
    def test_start_keywords(self):
        """Test queries with start/origin keywords"""
        start = (37.7749, -122.4194)
        end = (37.7849, -122.4094)
        mid = (37.7799, -122.4144)
        
        queries = [
            "gas stations near start",
            "parking at start",
            "restaurants beginning of trip",
            "hotels start of trip",
            "cafes departure"
        ]
        
        for query in queries:
            result = determine_search_location(query, start, end, mid)
            assert result == start, f"Query '{query}' should return start location"
    
    def test_midpoint_default(self):
        """Test queries default to midpoint"""
        start = (37.7749, -122.4194)
        end = (37.7849, -122.4094)
        mid = (37.7799, -122.4144)
        
        queries = [
            "restaurants along the route",
            "good coffee shops",
            "gas stations",
            "hotels"
        ]
        
        for query in queries:
            result = determine_search_location(query, start, end, mid)
            assert result == mid, f"Query '{query}' should return midpoint location"


class TestGeminiRecommendations:
    """Test Gemini AI recommendation logic"""
    
    @patch('main.genai.GenerativeModel')
    def test_gemini_recommendations_success(self, mock_model):
        """Test successful Gemini recommendations"""
        # Mock Gemini response
        mock_response = MagicMock()
        mock_response.text = '''
        {
            "recommendations": [
                {
                    "place_index": 0,
                    "reason": "Excellent rating and many positive reviews"
                },
                {
                    "place_index": 1,
                    "reason": "Great atmosphere and convenient location"
                }
            ]
        }
        '''
        
        mock_instance = MagicMock()
        mock_instance.generate_content.return_value = mock_response
        mock_model.return_value = mock_instance
        
        places = [
            {"name": "Test Restaurant", "rating": 4.5, "user_ratings_total": 100},
            {"name": "Test Cafe", "rating": 4.2, "user_ratings_total": 50}
        ]
        
        with patch('main.genai.configure'):
            result = get_gemini_recommendations(places, "good restaurants")
        
        assert "recommendations" in result
        assert len(result["recommendations"]) == 2
        assert result["recommendations"][0]["place_index"] == 0
        assert "Excellent rating" in result["recommendations"][0]["reason"]
    
    @patch('main.genai.GenerativeModel')
    def test_gemini_recommendations_fallback(self, mock_model):
        """Test Gemini fallback when AI fails"""
      
[truncated — 4318 more characters]
```

### server/test_integration.py

```python
"""
Integration tests for Semantic Maps Assistant API
Tests actual API behavior and integration between components
"""

import pytest
import json
import os
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

class TestAPIIntegration:
    """Integration tests for the full API workflow"""
    
    def test_health_endpoint_integration(self):
        """Test that health endpoint works end-to-end"""
        response = client.get("/")
        
        assert response.status_code == 200
        data = response.json()
        
        # Verify all expected fields are present
        expected_fields = ["message", "version", "ai_service", "maps_service"]
        for field in expected_fields:
            assert field in data
        
        # Verify correct values
        assert "Semantic Maps Assistant" in data["message"]
        assert data["ai_service"] == "Google Gemini AI"
        assert data["maps_service"] == "Google Maps"
    
    @patch.dict(os.environ, {
        'GOOGLE_API_KEY': 'test_google_key',
        'GEMINI_API_KEY': 'test_gemini_key'
    })
    @patch('main.googlemaps.Client')
    @patch('main.genai.GenerativeModel')
    @patch('main.genai.configure')
    def test_full_workflow_restaurant_search(self, mock_configure, mock_gemini, mock_gmaps):
        """Test complete workflow: query -> Gemini -> Maps -> recommendations"""
        
        # Mock Gemini AI response for query parsing
        mock_gemini_response = MagicMock()
        mock_gemini_response.text = json.dumps({
            "search_query": "restaurants",
            "place_type": "restaurant"
        })
        
        mock_gemini_instance = MagicMock()
        mock_gemini_instance.generate_content.return_value = mock_gemini_response
        mock_gemini.return_value = mock_gemini_instance
        
        # Mock Google Maps Places API response
        mock_places_response = {
            "results": [
                {
                    "place_id": "ChIJ_test_1",
                    "name": "Amazing Restaurant",
                    "geometry": {"location": {"lat": 37.7749, "lng": -122.4194}},
                    "rating": 4.8,
                    "user_ratings_total": 250,
                    "types": ["restaurant", "food"]
                },
                {
                    "place_id": "ChIJ_test_2",
                    "name": "Great Cafe",
                    "geometry": {"location": {"lat": 37.7750, "lng": -122.4195}},
                    "rating": 4.5,
                    "user_ratings_total": 150,
                    "types": ["cafe", "food"]
                }
            ]
        }
        
        # Mock Place Details API response
        mock_details_response = {
            "result": {
                "place_id": "ChIJ_test_1",
                "name": "Amazing Restaurant",
                "geometry": {"location": {"lat": 37.7749, "lng": -122.4194}},
                "rating": 4.8,
                "user_ratings_total": 250,
                "price_level": 3,
                "formatted_address": "123 Amazing St, San Francisco, CA",
                "formatted_phone_number": "(555) 123-4567",
                "website": "https://amazing-restaurant.com",
                "opening_hours": {
                    "weekday_text": ["Monday: 11:00 AM – 10:00 PM"]
                },
                "photos": [{"photo_reference": "test_photo_ref"}],
                "types": ["restaurant", "food"]
            }
        }
        
        # Configure Google Maps client mock
        mock_client = MagicMock()
        mock_client.places.return_value = mock_places_response
        mock_client.place.return_value = mock_details_response
        mock_gmaps.return_value = mock_client
        
        # Mock Gemini recommendations response
        mock_rec_response = MagicMock()
        mock_rec_response.text = json.dumps({
            "recommendations": [
                {
                    "place_index": 0,
                    "reason": "Exceptional rating of 4.8 stars with 250 reviews, indicating consistently excellent food and service"
                },
                {
                    "place_index": 1,
                    "reason": "Solid choice with 4.5 stars and good reputation in the area"
                }
            ]
        })
        
        # Mock second Gemini call for recommendations
        def mock_generate_content(prompt):
            if "travel expert analyzing places" in prompt:
                return mock_rec_response
            else:
                return mock_gemini_response
        
        mock_gemini_instance.generate_content.side_effect = mock_generate_content
        
        # Test request
        route_data = {
            "routes": [{
                "legs": [{
                    "start_location": {"lat": 37.7749, "lng": -122.4194},
                    "end_location": {"lat": 37.7849, "lng": -122.4094}
                }]
            }]
        }
        
        request_data = {
            "query": "best restaurants",
            "route": route_data
        }
        
        # Make request
        response = client.post("/api/find-places-on-route", json=request_data)
        
        # Verify response
        assert response.status_code == 200
        data = response.json()
        
        # Verify response structure
        assert "query" in data
        assert "search_location_type" in data
        assert "all_places" in data
        assert "recommended_places" in data
        assert "total_found" in data
        
        # Verify content
        assert data["query"] == "best restaurants"
        assert data["total_found"] > 0
        assert len(data["all_places"]) > 0
        assert len(data["recommended_places"]) > 0
        
        # Verify recommended place has AI reasoning
        rec_place = data["recommended_places"][0]
        assert "recommendation_reason" in rec_place
        assert "Excepti
[truncated — 4641 more characters]
```

[8 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]