← Back to all projects

📍 Location Finder - Ecuador

This project is a location finder that uses a local Nominatim server, filtering only results within Ecuador since I have locally configured Nominatim with data only from my country.

Preview of the Location Finder - Ecuador project

✨ Features

  • Created with FastAPI and a simple HTML frontend.
  • Redirects user requests to the local Nominatim server.
  • Displays results on an HTML page.
  • Allows viewing a history of searches.
  • Easily copy the coordinates of a location.
  • Option to open the location directly in Google Maps.
  • Displays the selected location on an interactive map using Leaflet.
  • Allows copying responses in JSON and XML formats.
  • Access to interactive API documentation via Swagger and ReDoc.

💡 Usage

  1. Enter an address or place in Ecuador.
  2. View the results and select the desired location.
  3. Observe the location on the interactive map.
  4. Copy the coordinates or the responses in JSON or XML format.
  5. Open the location in Google Maps or OSM.
  6. Check the recent search history.
  7. Access the API documentation at /docs (Swagger) or /redoc (ReDoc).
Preview of the Location Finder - Ecuador project

🤖 Scoring System

The project uses a technique to improve the relevance of autocomplete suggestions based on actual user usage. When a user selects a location, this "feedback" is recorded in a SQLite database.

This system assigns a popularity "boost" to the most selected results, causing them to appear at the top of future searches. In this way, the search engine "learns" which locations are most relevant to users in Ecuador. This process is similar to reinforcement learning, where the system optimizes itself based on user interactions.

Technical Operation

  • SQLite is used for persistent, long-term storage of user feedback.
  • A custom AutocompleteScorer combines popularity from the database with factors like fuzzy matching, Nominatim relevance, and place type to generate a final score for each suggestion.

Bootstrap Feedback

If you don't have users but want to simulate user responses or prioritize certain places, you can use the bootstrap_feedback.py script to generate initial feedback. This script allows you to load location data and assign an initial score, which helps establish a starting point for the scoring system.

import sys
import concurrent.futures
import threading
import requests
from fuzzywuzzy import fuzz
# Base URL of the FastAPI application
FASTAPI_BASE_URL = "http://127.0.0.1:8089"
# --- Path to the seed queries file ---
SEED_QUERIES_FILE = "seed_queries.txt"
# --- Thresholds for considering a suggestion "good" or "popular" ---
MIN_NOMINATIM_IMPORTANCE = 0.01
MIN_FUZZY_RATIO = 40
MIN_TOKEN_SET_RATIO = 70
# --- Threading Configuration ---
# Number of concurrent threads.
MAX_WORKERS = 10
# Lock to protect console printing (to avoid messy output)
print_lock = threading.Lock()
def load_seed_queries(file_path: str) -> list[str]:
"""Loads seed queries from a text file."""
queries = []
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
query = line.strip()
if query and not query.startswith(
"#"
): # Ignore empty lines or comments
queries.append(query)
except FileNotFoundError:
print(f"Error: The file '{file_path}' was not found.")
sys.exit(1) # Use sys.exit to exit the script
return queries
def record_simulated_selection(query: str, selected_item: dict):
"""Simulates sending feedback to the FastAPI endpoint."""
feedback_url = f"{FASTAPI_BASE_URL}/feedback"
payload = {
"query": query,
"selected_item": {
"osm_id": selected_item.get("osm_id"),
"display_name": selected_item.get("display_name"),
"lat": selected_item.get("lat"),
"lon": selected_item.get("lon"),
"type": selected_item.get("type"),
},
}
try:
response = requests.post(feedback_url, json=payload, timeout=5)
response.raise_for_status()
with print_lock: # Protect printing
print(
f" ✅ Successful simulation: '{selected_item.get('display_name')}' for query '{query}'"
)
return True # Indicate success
except requests.exceptions.RequestException as e:
with print_lock: # Protect printing
print(f" ❌ Error sending simulated feedback for '{query}': {e}")
return False # Indicate failure
def get_autocomplete_suggestions(query: str):
"""Gets suggestions from your own /autocomplete endpoint."""
autocomplete_url = f"{FASTAPI_BASE_URL}/autocomplete?query={query}"
try:
response = requests.get(autocomplete_url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
with print_lock: # Protect printing
print(f" Error getting suggestions from FastAPI for '{query}': {e}")
return []
def process_query(query: str):
"""
Function that processes a single query and simulates selection.
Designed to be run by a thread.
"""
suggestions = get_autocomplete_suggestions(query)
if not suggestions:
with print_lock:
print(f" ⚠️ No suggestions obtained from your API for '{query}'.")
return 0 # No selection was simulated
best_suggestion_for_query = None
highest_combined_bootstrap_score = -1
for item in suggestions:
display_name = item.get("display_name", "")
nominatim_importance = float(item.get("importance", 0.0))
query_lower = query.lower()
display_name_lower = display_name.lower()
fuzz_ratio = fuzz.ratio(query_lower, display_name_lower)
token_set_ratio = fuzz.token_set_ratio(query_lower, display_name_lower)
passes_importance = nominatim_importance >= MIN_NOMINATIM_IMPORTANCE
passes_fuzzy_or_token_set = (fuzz_ratio >= MIN_FUZZY_RATIO) or (
token_set_ratio >= MIN_TOKEN_SET_RATIO
)
current_bootstrap_score = (
(nominatim_importance * 0.4) + (token_set_ratio * 0.4) + (fuzz_ratio * 0.2)
)
with print_lock: # Protect printing to avoid console clutter
print(
f" - Suggestion: '{display_name}' (Imp: {nominatim_importance:.2f}, Fuzz: {fuzz_ratio}, TokenSet: {token_set_ratio})"
)
if passes_importance and passes_fuzzy_or_token_set:
print(
f" 👍 Qualifies: Imp={passes_importance}, (Fuzz={fuzz_ratio}>={MIN_FUZZY_RATIO} OR TokenSet={token_set_ratio}>={MIN_TOKEN_SET_RATIO})"
)
if current_bootstrap_score > highest_combined_bootstrap_score:
highest_combined_bootstrap_score = current_bootstrap_score
best_suggestion_for_query = item
else:
imp_fail = (
f"(Imp: {nominatim_importance:.2f} < {MIN_NOMINATIM_IMPORTANCE})"
if not passes_importance
else ""
)
fuzzy_fail = (
f"(Fuzz: {fuzz_ratio} < {MIN_FUZZY_RATIO})"
if (fuzz_ratio >= MIN_FUZZY_RATIO)
else ""
)
token_set_fail = (
f"(TokenSet: {token_set_ratio} < {MIN_TOKEN_SET_RATIO})"
if not (token_set_ratio >= MIN_TOKEN_SET_RATIO)
else ""
)
print(
f" 👎 Does not qualify: Imp: {passes_importance} {imp_fail}, Fuzz/TokenSet: {passes_fuzzy_or_token_set} ({fuzzy_fail} {token_set_fail})"
)
if best_suggestion_for_query:
record_simulated_selection(query, best_suggestion_for_query)
return 1 # 1 selection was simulated
else:
with print_lock:
print(
f" ❌ No 'good' suggestion found for '{query}' under the defined thresholds."
)
return 0 # No selection was simulated
def bootstrap_feedback_db():
"""
Simulates interactions to pre-populate the feedback database using threads.
"""
print("Starting feedback database bootstrapping...")
# Load seed queries from the file
seed_queries = load_seed_queries(SEED_QUERIES_FILE)
if not seed_queries:
print("No queries found in the seed file. Aborting.")
return
# Make sure the FastAPI server is running before executing this
try:
requests.get(f"{FASTAPI_BASE_URL}/docs", timeout=5)
print("FastAPI server accessible.")
except requests.exceptions.ConnectionError:
print("Error: The FastAPI server is not running at", FASTAPI_BASE_URL)
print("Please start your FastAPI application before running this script.")
return
total_simulated_selections = 0
# Use ThreadPoolExecutor to run queries in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
# Map the process_query function to each seed query
# as_completed returns results as threads finish
futures = {
executor.submit(process_query, query): query for query in seed_queries
}
for future in concurrent.futures.as_completed(futures):
query = futures[future]
try:
# Each thread returns 1 if a selection was simulated, 0 otherwise.
total_simulated_selections += future.result()
except concurrent.futures.CancelledError as exc:
with print_lock:
print(f"The query '{query}' was cancelled: {exc}")
except concurrent.futures.TimeoutError as exc:
with print_lock:
print(f"The query '{query}' exceeded the timeout: {exc}")
except ValueError as exc:
with print_lock:
print(f"The query '{query}' generated a ValueError: {exc}")
except TypeError as exc:
with print_lock:
print(f"The query '{query}' generated a TypeError: {exc}")
except requests.exceptions.RequestException as exc:
with print_lock:
print(f"The query '{query}' generated an HTTP request error: {exc}")
print("--- Bootstrapping Complete ---")
print(f"Total simulated selections: {total_simulated_selections}")
# Note: The DB_PATH is not printed directly from here, as cache.py already does it.
print("The feedback database has been updated.")
print("--- ATTENTION! ---")
print(f"Concurrency was used with {MAX_WORKERS} threads.")
print(
"This may cause your FastAPI server's rate limits to be reached more quickly."
)
print(
"If you see many 429 errors, consider reducing the number of MAX_WORKERS or increasing the limits in your 'rate_limiter.py'."
)
print(
"You can also adjust the thresholds (MIN_NOMINATIM_IMPORTANCE, MIN_FUZZY_RATIO, MIN_TOKEN_SET_RATIO) in this script."
)
print(f"You can add/modify queries in '{SEED_QUERIES_FILE}'.")
if __name__ == "__main__":
try:
_ = fuzz.ratio("test", "test")
except ImportError:
print("Error: The 'fuzzywuzzy' library is not installed.")
print("Install it with: pip install fuzzywuzzy python-Levenshtein")
sys.exit(1)
bootstrap_feedback_db()
python

🚀 Query Caching with Redis

To speed up responses and reduce the load on the local Nominatim server, a short-term cache layer is implemented using Redis.

  • Upon receiving a query, the API first attempts to get the result from the Redis cache.
  • If the query has been made recently (within 5 minutes), the response is served instantly from the cache.
  • If there is no response in the cache, the query is made to the Nominatim server, processed, and the final result is saved in Redis for future requests.

This strategy significantly improves performance for repeated searches, providing a faster and more efficient user experience.

🛠️ Requirements

  • Python 3.8+
  • FastAPI
  • Local Nominatim server (although this one is configured only with data from Ecuador)

📦 Installation

pip install fastapi uvicorn requests python-multipart
bash

🚀 Execution

uvicorn main:app --reload
bash

⚠️ Disclaimer

This project is designed solely for personal use or in controlled environments with a local Nominatim server configured with data from Ecuador.

Warning:

Do not use this search engine with the original Nominatim server or with platforms that do not allow the use of autocompletion or automated query redirection. Misuse may violate the terms of service and result in blocks or penalties.