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():
    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])   # all coordinates
    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, dist 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(dist)
        })

    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():
    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])
    y = np.array([point["category"] for point 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_category = knn_classifier.predict(query_point)[0]
    probabilities = knn_classifier.predict_proba(query_point)[0]
    class_names = knn_classifier.classes_

    category_points = [point for point in data_points if point["category"] == predicted_category]
    similar_points = []
    if category_points:
        X_category = np.array([[point["lat"], point["lng"]] for point in category_points])
        category_knn = NearestNeighbors(n_neighbors=min(k, len(category_points)), metric='haversine')
        category_knn.fit(X_category)
        distances, indices = category_knn.kneighbors(query_point)
        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 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