Spaces:
Sleeping
Sleeping
Commit
·
74c43d2
1
Parent(s):
f5346bf
added core geotagging logic & api
Browse files
core.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core logic for fetching geotags from addresses or coordinates."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import List
|
| 5 |
+
import httpx
|
| 6 |
+
import h3
|
| 7 |
+
from .models import GeotagResult
|
| 8 |
+
|
| 9 |
+
# Regex for validating "latitude,longitude" input
|
| 10 |
+
COORD_REGEX = re.compile(r"^-?([1-8]?[0-9]|[1-9]0)\.{1}\d{1,15}\s*,\s*-?([1]?[0-7]?[0-9]|[1]?[1-8]?[0])\.{1}\d{1,15}$")
|
| 11 |
+
|
| 12 |
+
class GeotaggingError(Exception):
|
| 13 |
+
"""Base exception for geotagging errors."""
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
class InvalidCoordinatesError(GeotaggingError):
|
| 17 |
+
"""Raised when coordinate input is invalid."""
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
class GeocodingServiceError(GeotaggingError):
|
| 21 |
+
"""Raised when the external geocoding service fails."""
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
class NoResultsFoundError(GeotaggingError):
|
| 25 |
+
"""Raised when no results are found for a query."""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
async def fetch_geotags(q: str, resolution: int) -> List[GeotagResult]:
|
| 30 |
+
"""
|
| 31 |
+
Fetches geotag information for a given query string (address or lat,lng).
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
q: The address string or 'latitude,longitude' pair.
|
| 35 |
+
resolution: The H3 resolution (0-15).
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
A list of GeotagResult objects.
|
| 39 |
+
|
| 40 |
+
Raises:
|
| 41 |
+
InvalidCoordinatesError: If the input coordinates are malformed.
|
| 42 |
+
GeocodingServiceError: If there's an issue connecting to the geocoding service.
|
| 43 |
+
NoResultsFoundError: If the geocoding service returns no results.
|
| 44 |
+
"""
|
| 45 |
+
locations = []
|
| 46 |
+
|
| 47 |
+
if COORD_REGEX.match(q.replace(" ", "")):
|
| 48 |
+
try:
|
| 49 |
+
lat_str, lon_str = q.split(',')
|
| 50 |
+
lat, lon = float(lat_str), float(lon_str)
|
| 51 |
+
locations.append({
|
| 52 |
+
"lat": lat,
|
| 53 |
+
"lon": lon,
|
| 54 |
+
"display_name": f"Coordinates: {lat:.6f}, {lon:.6f}"
|
| 55 |
+
})
|
| 56 |
+
except (ValueError, IndexError):
|
| 57 |
+
raise InvalidCoordinatesError("Invalid coordinate format. Use 'latitude,longitude'.")
|
| 58 |
+
else:
|
| 59 |
+
headers = {'User-Agent': 'TrackEmDown/1.0 (nikhilsingh.io)'}
|
| 60 |
+
url = f"https://nominatim.openstreetmap.org/search?format=json&q={q}"
|
| 61 |
+
|
| 62 |
+
async with httpx.AsyncClient() as client:
|
| 63 |
+
try:
|
| 64 |
+
response = await client.get(url, headers=headers, timeout=10.0)
|
| 65 |
+
response.raise_for_status()
|
| 66 |
+
locations = response.json()
|
| 67 |
+
except httpx.RequestError as exc:
|
| 68 |
+
raise GeocodingServiceError(f"Error connecting to geocoding service: {exc}")
|
| 69 |
+
|
| 70 |
+
if not locations:
|
| 71 |
+
raise NoResultsFoundError("No results found for the given query.")
|
| 72 |
+
|
| 73 |
+
results = []
|
| 74 |
+
for loc in locations:
|
| 75 |
+
try:
|
| 76 |
+
lat, lon = float(loc['lat']), float(loc['lon'])
|
| 77 |
+
geotag = h3.latlng_to_cell(lat, lon, resolution)
|
| 78 |
+
results.append(
|
| 79 |
+
GeotagResult(
|
| 80 |
+
address=loc.get('display_name', 'N/A'),
|
| 81 |
+
latitude=lat,
|
| 82 |
+
longitude=lon,
|
| 83 |
+
geotag=geotag
|
| 84 |
+
)
|
| 85 |
+
)
|
| 86 |
+
except (ValueError, KeyError) as e:
|
| 87 |
+
# Skip malformed results from the external API
|
| 88 |
+
print(f"Skipping a location due to parsing error: {e}")
|
| 89 |
+
continue
|
| 90 |
+
|
| 91 |
+
if not results:
|
| 92 |
+
raise NoResultsFoundError("Geocoding service returned malformed data.")
|
| 93 |
+
|
| 94 |
+
return results
|
main.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI application for the Geotagging Service."""
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 4 |
+
from .models import GeotagResponse
|
| 5 |
+
from .core import fetch_geotags, NoResultsFoundError, GeocodingServiceError, InvalidCoordinatesError
|
| 6 |
+
|
| 7 |
+
app = FastAPI(
|
| 8 |
+
title="Geotagging Service",
|
| 9 |
+
description="An API to convert addresses and coordinates to Uber H3 geotags using OSM Nominatim.",
|
| 10 |
+
version="1.0.0",
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
@app.get(
|
| 14 |
+
"/geotag",
|
| 15 |
+
response_model=GeotagResponse,
|
| 16 |
+
summary="Get H3 geotag for an address or coordinates",
|
| 17 |
+
tags=["Geotagging"],
|
| 18 |
+
)
|
| 19 |
+
async def get_geotag(
|
| 20 |
+
q: str = Query(..., description="The address string or 'latitude,longitude' pair to look up."),
|
| 21 |
+
resolution: int = Query(12, ge=0, le=15, description="The H3 resolution (0-15). Default is 12.")
|
| 22 |
+
):
|
| 23 |
+
"""
|
| 24 |
+
This endpoint takes a query string `q` and an H3 `resolution` and returns a list of matching geotags.
|
| 25 |
+
|
| 26 |
+
- If `q` is a valid "latitude,longitude" string, it's converted directly.
|
| 27 |
+
- If `q` is an address, it's sent to the OpenStreetMap Nominatim API for geocoding.
|
| 28 |
+
- The resulting coordinates are then converted into H3 cell identifiers (geotags).
|
| 29 |
+
"""
|
| 30 |
+
try:
|
| 31 |
+
results = await fetch_geotags(q, resolution)
|
| 32 |
+
return GeotagResponse(
|
| 33 |
+
query=q,
|
| 34 |
+
resolution=resolution,
|
| 35 |
+
results=results,
|
| 36 |
+
)
|
| 37 |
+
except InvalidCoordinatesError as e:
|
| 38 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 39 |
+
except NoResultsFoundError as e:
|
| 40 |
+
raise HTTPException(status_code=404, detail=str(e))
|
| 41 |
+
except GeocodingServiceError as e:
|
| 42 |
+
raise HTTPException(status_code=503, detail=str(e))
|
| 43 |
+
except Exception as e:
|
| 44 |
+
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {e}")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@app.get("/", include_in_schema=False)
|
| 48 |
+
async def root():
|
| 49 |
+
"""Root endpoint for health check."""
|
| 50 |
+
return {"status": "ok", "message": "Geotagging Service is running."}
|