nikhilsingh commited on
Commit
74c43d2
·
1 Parent(s): f5346bf

added core geotagging logic & api

Browse files
Files changed (2) hide show
  1. core.py +94 -0
  2. main.py +50 -0
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."}