Detailed Step-by-Step Tutorial: Building the KNN + Google Maps Flask App

We will build this project incrementally, explaining every piece of code as we go.
You’ll see small code snippets first (with explanations), then at the very end I’ll give you the complete ready-to-copy files.


Step 1: Get Your Google Maps API Key (5 minutes)

  1. Go to Google Cloud Console.
  2. Create a new project → Enable Maps JavaScript API.
  3. Create an API key and restrict it to “Maps JavaScript API” only.
  4. Copy the key (starts with AIza...).

We will store it in a .env file (never commit it to Git).


Step 2: Set Up the Project Folder

mkdir knn-google-maps
cd knn-google-maps
python -m venv venv
# OR
# python3 -m venv venv

# Activate:
#   macOS/Linux → source venv/bin/activate
#   Windows    → venv\Scripts\activate

Create the exact folder structure:

knn-google-maps/
├── templates/          ← create this folder
│   └── index.html      ← will create later
├── .env                ← will create later
├── .gitignore          ← will create later
└── app.py              ← will create later

Step 3: Install Dependencies

pip install flask python-dotenv scikit-learn numpy

Step 4: Create .env (store the API key)

Snippet (create file .env):

GOOGLE_MAPS_KEY=AIzaSy...your_actual_key_here...

Step 5: Build app.py – Piece by Piece

5.1 Imports & Flask Setup

from flask import Flask, render_template, request, jsonify
import os
from dotenv import load_dotenv
from sklearn.neighbors import NearestNeighbors, KNeighborsClassifier
import numpy as np

load_dotenv()          # loads GOOGLE_MAPS_KEY from .env

app = Flask(__name__)
  • Flask → web framework
  • render_template → serves index.html
  • dotenv → reads the API key securely
  • sklearn → provides KNN search and classification
  • numpy → handles coordinate arrays

5.2 Cache-Control Header (development helper)

@app.after_request
def add_cache_headers(response):
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, public, max-age=0'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response

Explanation: Prevents the browser from caching old JavaScript/CSS while you develop.

5.3 Custom Data Points (the heart of the demo)

data_points = [
    # Restaurants – red markers
    {"lat": 40.4168, "lng": -3.7038, "title": "Puerta del Sol", "description": "The heart of Madrid 🌞", "icon": "https://maps.google.com/mapfiles/ms/icons/red-dot.png", "category": "restaurant"},
    # ... (3 more restaurants)

    # Car workshops – green markers
    {"lat": 40.4075, "lng": -3.6925, "title": "Retiro Park", "description": "Beautiful park with lake and monuments", "icon": "https://maps.google.com/mapfiles/ms/icons/green-dot.png", "category": "car_workshop"},
    # ... (3 more car workshops)
]

Explanation: Each point has coordinates, display info, Google Maps icon, and a category used for classification. You can add as many points as you want.

5.4 Home Route (/)

@app.route("/")
def index():
    return render_template(
        "index.html",
        google_maps_key=os.getenv("GOOGLE_MAPS_KEY"),
        points=data_points
    )

Explanation: Renders the map page and passes the API key + data points to the template (Jinja2).

5.5 KNN Search Endpoint

@app.route("/knn_search", methods=["POST"])
def knn_search():
    # Get lat, lng, and k from the JSON body of the POST request:
    data = request.get_json()
    lat = data.get("lat");
    lng = data.get("lng");
    k = data.get("k");
    if lat is None or lng is None or k is None:
        return jsonify({"error": "Missing lat/lng/k"}), 400

    # Extract all latitude and longitude coordinates from data_points into a 2D numpy array
    # Shape: (num_points, 2) where each row is [latitude, longitude]
    X = np.array([[p["lat"], p["lng"]] for p in data_points]);
    # Uncomment the following lines to see the coordinates array in the console
    # print("Coordinates array X:")
    # print(X)

    # Create a KNN model using haversine distance (great-circle distance for lat/lng)
    # min(k, len(data_points)) ensures we don't ask for more neighbors than available points
    knn = NearestNeighbors(n_neighbors=min(k, len(data_points)), metric='haversine')
    
    # Train the KNN model on our coordinate data
    knn.fit(X)

    # Convert the user's query location into the same 2D array format as training data
    query_point = np.array([[lat, lng]])
    
    # Find the k nearest neighbors to the query point
    # Returns distances (in radians) and indices of the nearest points in data_points
    distances, indices = knn.kneighbors(query_point)
    
    # print("Distances (in radians):", distances);
    # print("Neighbor indices:", indices);


    # Prepare the result list by mapping each neighbor index back to the original data point
    # indices[0][i] corresponds to distances[0][i], so we zip them together
    neighbors = []
    for idx, dist in zip(indices[0], distances[0]):
        point = data_points[int(idx)]

        # Create a clean result object for frontend consumption
        neighbors.append({
            "lat": point["lat"],
            "lng": point["lng"],
            "title": point["title"],
            "description": point["description"],
            # Convert NumPy scalar to plain Python float for JSON serialization
            "distance": float(dist)
        })

    # Return query and neighbor data to the client as JSON
    return jsonify({"query": {"lat": lat, "lng": lng}, "neighbors": neighbors})

Explanation:

  • haversine metric = correct distance on a sphere (Earth).
  • Returns the k closest real data points with their distance in degrees.

5.6 KNN Classification Endpoint

@app.route("/knn_classification", methods=["POST"])
def knn_classification():
    data = request.json
    lat, lng = data.get("lat"), data.get("lng")
    k = data.get("k", 3)

    if lat is None or lng is None:
        return jsonify({"error": "Missing lat/lng"}), 400

    X = np.array([[p["lat"], p["lng"]] for p in data_points])
    y = np.array([p["category"] for p in data_points])

    knn_classifier = KNeighborsClassifier(n_neighbors=min(k, len(data_points)))
    knn_classifier.fit(X, y)

    query_point = np.array([[lat, lng]])
    predicted = knn_classifier.predict(query_point)[0]
    proba = knn_classifier.predict_proba(query_point)[0]
    classes = knn_classifier.classes_

    # Find similar points inside the predicted category
    category_points = [p for p in data_points if p["category"] == predicted]
    similar = []
    if category_points:
        X_cat = np.array([[p["lat"], p["lng"]] for p in category_points])
        cat_knn = NearestNeighbors(n_neighbors=min(k, len(category_points)), metric='haversine')
        cat_knn.fit(X_cat)
        dists, idxs = cat_knn.kneighbors(query_point)
        for i, d in zip(idxs[0], dists[0]):
            p = category_points[int(i)]
            similar.append({"lat": p["lat"], "lng": p["lng"], "title": p["title"],
                            "description": p["description"], "distance": float(d)})

    return jsonify({
        "query": {"lat": lat, "lng": lng},
        "predicted_category": predicted,
        "probabilities": {classes[0]: float(proba[0]), classes[1]: float(proba[1])},
        "similar_points": similar
    })

Explanation:

  • Trains a classifier on the labeled points.
  • Predicts the category of the clicked point.
  • Returns confidence percentages + the k nearest points of that category.

5.7 Run the app

if __name__ == "__main__":
    app.run(debug=True)

Step 6: Build templates/index.html – Piece by Piece

6.1 Structure

(Full style block is in the complete file at the end – it creates the floating panel.)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Python + Google Maps</title>
    <style>
        ...
    </style>
</head>
<body>

<div class="header">
  ...
</div>

<div id="map"></div>

<script>
  ...
</script>

<script async src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_key }}&callback=initMap"></script>
</body>
</html>

6.2. Header

<div class="header">
    <h2>🚀 Python Flask + Google Maps</h2>
    <p>{{ points|length }} custom data points loaded from Python</p>
    
    <div class="controls">
        <label>Mode:</label>
        <div style="display: flex; gap: 10px;">
            <label><input type="radio" name="mode" value="search" checked> Search</label>
            <label><input type="radio" name="mode" value="classification"> Classification</label>
        </div>
    </div>
    
    <div class="controls">
        <label for="kNeighbors">K Neighbors:</label>
        <input type="number" id="kNeighbors" value="3" min="1" max="10">
    </div>
    
    <div id="results" class="results" style="display: none;">
        <strong id="resultsTitle">KNN Results:</strong>
        <div id="resultsList"></div>
    </div>
    <p style="font-size: 12px; color: #666; margin-top: 10px;">
        <span id="instructionText">Click on the map to find nearest neighbors</span>
    </p>
</div>

6.3 JavaScript – Map Initialization

function initMap() {
    map = new google.maps.Map(document.getElementById("map"), {
        zoom: 13,
        center: { lat: 40.4168, lng: -3.7038 }
    });

    const mapPoints = {{ points | tojson | safe }};   // data from Flask

    mapPoints.forEach(p => {
        const marker = new google.maps.Marker({
            position: { lat: p.lat, lng: p.lng },
            map: map,
            title: p.title,
            icon: p.icon
        });
        // Info window on click
        const info = new google.maps.InfoWindow({ content: `<h3>${p.title}</h3><p>${p.description}</p>` });
        marker.addListener("click", () => info.open(map, marker));
    });

    // Click anywhere → trigger KNN
    map.addListener("click", e => {
        const mode = document.querySelector('input[name="mode"]:checked').value;
        if (mode === "search") performKNNSearch(e.latLng.lat(), e.latLng.lng());
        else performKNNClassification(e.latLng.lat(), e.latLng.lng());
    });
}

6.4 The Two Fetch Functions (search & classification)

(Shown in complete file – they send POST to /knn_search or /knn_classification.)

6.5 Display Functions

  • displaySearchResults() → shows numbered markers + list
  • displayClassificationResults() → shows predicted category, confidence %, and similar points

Step 7: Run It!

python app.py

Open http://127.0.0.1:5000 → you should see the exact app from your screenshot.


Complete Files (copy-paste ready)

1. app.py (full file)

from flask import Flask, render_template, request, jsonify
import os
from dotenv import load_dotenv
from sklearn.neighbors import NearestNeighbors, KNeighborsClassifier
import numpy as np

load_dotenv()

app = Flask(__name__)

@app.after_request
def add_cache_headers(response):
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, public, max-age=0'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response

# === YOUR CUSTOM DATA POINTS ===
data_points = [
    {"lat": 40.4168, "lng": -3.7038, "title": "Puerta del Sol", "description": "The heart of Madrid 🌞", "icon": "https://maps.google.com/mapfiles/ms/icons/red-dot.png", "category": "restaurant"},
    {"lat": 40.4154, "lng": -3.7074, "title": "Plaza Mayor", "description": "Historic square with beautiful architecture", "icon": "https://maps.google.com/mapfiles/ms/icons/red-dot.png", "category": "restaurant"},
    {"lat": 40.4140, "lng": -3.7010, "title": "Casa Botín", "description": "Oldest restaurant in the world 🍽️", "icon": "https://maps.google.com/mapfiles/ms/icons/red-dot.png", "category": "restaurant"},
    {"lat": 40.4175, "lng": -3.7050, "title": "La Latina District", "description": "Famous for tapas and restaurants", "icon": "https://maps.google.com/mapfiles/ms/icons/red-dot.png", "category": "restaurant"},
    {"lat": 40.4075, "lng": -3.6925, "title": "Retiro Park", "description": "Beautiful park with lake and monuments", "icon": "https://maps.google.com/mapfiles/ms/icons/green-dot.png", "category": "car_workshop"},
    {"lat": 40.4304, "lng": -3.7023, "title": "Santiago Bernabéu Stadium", "description": "Home of Real Madrid ⚽", "icon": "https://maps.google.com/mapfiles/ms/icons/green-dot.png", "category": "car_workshop"},
    {"lat": 40.4050, "lng": -3.6900, "title": "AutoZone Madrid", "description": "Car parts and service center 🔧", "icon": "https://maps.google.com/mapfiles/ms/icons/green-dot.png", "category": "car_workshop"},
    {"lat": 40.4320, "lng": -3.7000, "title": "Bernabéu Auto Service", "description": "Professional car repair shop", "icon": "https://maps.google.com/mapfiles/ms/icons/green-dot.png", "category": "car_workshop"},
]

@app.route("/")
def index():
    return render_template(
        "index.html",
        google_maps_key=os.getenv("GOOGLE_MAPS_KEY"),
        points=data_points
    )

@app.route("/knn_search", methods=["POST"])
def knn_search():
    data = request.json
    lat = data.get("lat")
    lng = data.get("lng")
    k = data.get("k", 3)
    if lat is None or lng is None:
        return jsonify({"error": "Missing lat/lng"}), 400

    X = np.array([[point["lat"], point["lng"]] for point in data_points])
    knn = NearestNeighbors(n_neighbors=min(k, len(data_points)), metric='haversine')
    knn.fit(X)
    query_point = np.array([[lat, lng]])
    distances, indices = knn.kneighbors(query_point)

    neighbors = []
    for idx, distance in zip(indices[0], distances[0]):
        point = data_points[int(idx)]
        neighbors.append({
            "lat": point["lat"], "lng": point["lng"],
            "title": point["title"], "description": point["description"],
            "distance": float(distance)
        })
    return jsonify({"query": {"lat": lat, "lng": lng}, "neighbors": neighbors})

@app.route("/knn_classification", methods=["POST"])
def knn_classification():
    # Get lat, lng, and k from the JSON body of the POST request:
    data = request.json
    lat = data.get("lat")
    lng = data.get("lng")
    k = data.get("k", 3)
    if lat is None or lng is None or k is None:
        return jsonify({"error": "Missing lat/lng/k"}), 400

    # X: Feature matrix - 2D array where each row is a location's [latitude, longitude]
    # Shape: (n_samples, 2) - used as input features for the classifier
    X = np.array([[point["lat"], point["lng"]] for point in data_points])
    
    # y: Target labels - 1D array of category strings for each location
    # Shape: (n_samples,) - the classes we want to predict (e.g., "restaurant", "car_workshop")
    y = np.array([point["category"] for point in data_points])

    # Create a KNN classifier that will predict categories based on location proximity
    # Uses Haversine distance, which is appropriate for geographic coordinates (lat/lng)
    # min(k, len(data_points)) ensures we don't ask for more neighbors than available points
    knn_classifier = KNeighborsClassifier(n_neighbors=min(k, len(data_points)), metric='haversine')
    
    # Train the classifier on our feature matrix X and target labels y
    knn_classifier.fit(X, y)

    # Convert query location to same format as training data
    query_point = np.array([[lat, lng]])
    
    # Predict the most likely category for the query location
    predicted_category = knn_classifier.predict(query_point)[0]
    
    # Get probability scores for all possible categories at this location
    probabilities = knn_classifier.predict_proba(query_point)[0]
    
    # Get the list of all possible category names (classes) the model was trained on
    class_names = knn_classifier.classes_

    # Filter data points to only include those in the predicted category
    category_points = [point for point in data_points if point["category"] == predicted_category]
    
    # Find the k nearest neighbors within the predicted category for more relevant results
    similar_points = []
    if category_points:
        # Create coordinate array for points in the predicted category only
        X_category = np.array([[point["lat"], point["lng"]] for point in category_points])
        
        # Use NearestNeighbors (not classifier) to find geographically closest points in this category
        category_knn = NearestNeighbors(n_neighbors=min(k, len(category_points)), metric='haversine')
        category_knn.fit(X_category)
        
        # Find nearest neighbors within the category
        distances, indices = category_knn.kneighbors(query_point)
        
        # Build result list of similar points in the predicted category
        for idx, distance in zip(indices[0], distances[0]):
            point = category_points[int(idx)]
            similar_points.append({
                "lat": point["lat"],
                "lng": point["lng"],
                "title": point["title"],
                "description": point["description"],
                "distance": float(distance)
            })

    # Return comprehensive results: prediction, confidence scores, and similar locations
    return jsonify({
        "query": {"lat": lat, "lng": lng},
        "predicted_category": predicted_category,
        "probabilities": {class_names[0]: float(probabilities[0]), class_names[1]: float(probabilities[1])},
        "similar_points": similar_points
    })

if __name__ == "__main__":
    app.run(debug=True)

2. templates/index.html (full file)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Python + Google Maps</title>
    <style>
        body { margin: 0; font-family: Arial, sans-serif; }
        #map { height: 100vh; width: 100%; }
        .header {
            position: absolute; top: 10px; left: 10px; background: white;
            padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 1000;
        }
        .controls { background: white; padding: 12px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); margin-top: 10px; display: flex; gap: 10px; align-items: center; }
        .results { background: white; padding: 12px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); margin-top: 10px; font-size: 12px; max-height: 200px; overflow-y: auto; }
        .result-item { padding: 8px; border-bottom: 1px solid #eee; background: #f9f9f9; }
        .result-item:last-child { border-bottom: none; }
        .result-title { font-weight: bold; color: #333; }
        .result-distance { color: #666; font-size: 11px; }
    </style>
</head>
<body>

<div class="header">
    <h2>🚀 Python Flask + Google Maps</h2>
    <p>{{ points|length }} custom data points loaded from Python</p>
    
    <div class="controls">
        <label>Mode:</label>
        <div style="display: flex; gap: 10px;">
            <label><input type="radio" name="mode" value="search" checked> Search</label>
            <label><input type="radio" name="mode" value="classification"> Classification</label>
        </div>
    </div>
    
    <div class="controls">
        <label for="kNeighbors">K Neighbors:</label>
        <input type="number" id="kNeighbors" value="3" min="1" max="10">
    </div>
    
    <div id="results" class="results" style="display: none;">
        <strong id="resultsTitle">KNN Results:</strong>
        <div id="resultsList"></div>
    </div>
    <p style="font-size: 12px; color: #666; margin-top: 10px;">
        <span id="instructionText">Click on the map to find nearest neighbors</span>
    </p>
</div>

<div id="map"></div>

<script>
    let map, clickMarker = null, resultMarkers = [];

    function initMap() {
        map = new google.maps.Map(document.getElementById("map"), {
            zoom: 13,
            center: { lat: 40.4168, lng: -3.7038 },
            mapTypeId: "roadmap"
        });

        const mapPoints = {{ points | tojson | safe }};
        mapPoints.forEach(p => {
            const marker = new google.maps.Marker({
                position: { lat: p.lat, lng: p.lng },
                map: map,
                title: p.title,
                icon: p.icon
            });
            const info = new google.maps.InfoWindow({ content: `<h3>${p.title}</h3><p>${p.description}</p>` });
            marker.addListener("click", () => info.open(map, marker));
        });

        map.addListener("click", e => {
            const mode = document.querySelector('input[name="mode"]:checked').value;
            if (mode === "search") performKNNSearch(e.latLng.lat(), e.latLng.lng());
            else performKNNClassification(e.latLng.lat(), e.latLng.lng());
        });
    }

    function performKNNSearch(lat, lng) {
        const k = parseInt(document.getElementById("kNeighbors").value) || 3;
        if (clickMarker) clickMarker.setMap(null);
        clickMarker = new google.maps.Marker({ position: { lat, lng }, map, icon: "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png" });

        fetch("/knn_search", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ lat, lng, k })
        })
        .then(r => r.json())
        .then(data => displaySearchResults(data.neighbors));
    }

    function performKNNClassification(lat, lng) {
        const k = parseInt(document.getElementById("kNeighbors").value) || 3;
        if (clickMarker) clickMarker.setMap(null);
        clickMarker = new google.maps.Marker({ position: { lat, lng }, map, icon: "http://maps.google.com/mapfiles/ms/icons/yellow-dot.png" });

        fetch("/knn_classification", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ lat, lng, k })
        })
        .then(r => r.json())
        .then(data => displayClassificationResults(data));
    }

    function clearResults() {
        resultMarkers.forEach(m => m.setMap(null));
        resultMarkers = [];
        document.getElementById("results").style.display = "none";
    }

    function displaySearchResults(neighbors) {
        clearResults();
        const list = document.getElementById("resultsList");
        list.innerHTML = "";
        neighbors.forEach((n, i) => {
            const marker = new google.maps.Marker({ position: { lat: n.lat, lng: n.lng }, map, label: String(i+1) });
            resultMarkers.push(marker);

            const div = document.createElement("div");
            div.className = "result-item";
            div.innerHTML = `
                <div class="result-title">${i+1}. ${n.title}</div>
                <div class="result-distance">Distance: ${n.distance.toFixed(4)}°</div>
                <div style="font-size:11px;color:#999;">${n.description}</div>`;
            list.appendChild(div);
        });
        document.getElementById("results").style.display = "block";
    }

    function displayClassificationResults(data) {
        clearResults();
        const list = document.getElementById("resultsList");
        list.innerHTML = "";

        // Prediction
        const predDiv = document.createElement("div");
        predDiv.className = "result-item";
        predDiv.innerHTML = `
            <div class="result-title" style="color:#2e7d32;">Predicted Category: ${data.predicted_category}</div>
            <div style="font-size:12px;margin:5px 0;">
                <strong>Confidence:</strong><br>
                Restaurant: ${(data.probabilities.restaurant*100).toFixed(1)}%<br>
                Car Workshop: ${(data.probabilities.car_workshop*100).toFixed(1)}%
            </div>`;
        list.appendChild(predDiv);

        // Similar points
        if (data.similar_points.length) {
            const header = document.createElement("div");
            header.className = "result-item";
            header.innerHTML = `<div class="result-title">Similar ${data.predicted_category} locations:</div>`;
            list.appendChild(header);

            data.similar_points.forEach((p, i) => {
                const marker = new google.maps.Marker({
                    position: { lat: p.lat, lng: p.lng },
                    map,
                    label: String(i+1),
                    icon: data.predicted_category === "restaurant" ? 
                        "http://maps.google.com/mapfiles/ms/icons/red-dot.png" : 
                        "http://maps.google.com/mapfiles/ms/icons/green-dot.png"
                });
                resultMarkers.push(marker);

                const div = document.createElement("div");
                div.className = "result-item";
                div.innerHTML = `
                    <div class="result-title">${i+1}. ${p.title}</div>
                    <div class="result-distance">Distance: ${p.distance.toFixed(4)}°</div>
                    <div style="font-size:11px;color:#999;">${p.description}</div>`;
                list.appendChild(div);
            });
        }
        document.getElementById("results").style.display = "block";
    }

    // Mode toggle
    window.addEventListener('load', () => {
        document.querySelectorAll('input[name="mode"]').forEach(r => {
            r.addEventListener('change', () => {
                document.getElementById("instructionText").textContent = 
                    r.value === "search" ? "Click on the map to find nearest neighbors" : "Click on the map to classify the location";
                document.getElementById("resultsTitle").textContent = 
                    r.value === "search" ? "KNN Search Results:" : "KNN Classification Results:";
                clearResults();
            });
        });
    });
</script>

<script async src="https://maps.googleapis.com/maps/api/js?key={{ google_maps_key }}&callback=initMap"></script>
</body>
</html>

3. .env (example)

GOOGLE_MAPS_KEY=AIzaSy...your_key...

4. .gitignore (recommended)

venv/
__pycache__/
.env

You now have a fully explained, working project!
Just replace the API key in .env, run python app.py, and start clicking on the map.

Want to add more categories, change the map style, or export results to CSV? Let me know and I’ll extend the tutorial! 🚀

Leave a Reply