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)
- Go to Google Cloud Console.
- Create a new project → Enable Maps JavaScript API.
- Create an API key and restrict it to “Maps JavaScript API” only.
- 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 frameworkrender_template→ servesindex.htmldotenv→ reads the API key securelysklearn→ provides KNN search and classificationnumpy→ 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 responseExplanation: 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:
haversinemetric = correct distance on a sphere (Earth).- Returns the
kclosest 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
knearest 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 + listdisplayClassificationResults()→ 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! 🚀