Skip to content

Commit f6196c9

Browse files
committed
fix bugs in icon, search and location
1 parent 0347258 commit f6196c9

File tree

9 files changed

+331
-77
lines changed

9 files changed

+331
-77
lines changed

backend/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,12 @@ Env vars:
3737
- `GEOCODING_USER_AGENT` (default `EasyRelocate/0.1 (local dev)`)
3838
- `NOMINATIM_BASE_URL` (default `https://nominatim.openstreetmap.org`)
3939
- `GEOCODING_TIMEOUT_S` (default `6`)
40+
41+
### Google setup requirements
42+
If you use Google geocoding (`GEOCODING_PROVIDER=google` or `GOOGLE_MAPS_API_KEY` is set):
43+
- Enable **billing** for your Google Cloud project (Google Maps Platform)
44+
- Enable the **Geocoding API** in Google Cloud Console for that project
45+
46+
If you see:
47+
`Google Geocoding failed with status REQUEST_DENIED: This API is not activated on your API project`
48+
it means the Geocoding API is not enabled (or billing/key restrictions are blocking it).

backend/app/geocoding.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
2020

2121

22+
class GeocodingError(RuntimeError):
23+
pass
24+
25+
26+
class GeocodingConfigError(GeocodingError):
27+
pass
28+
29+
30+
class GeocodingProviderError(GeocodingError):
31+
pass
32+
33+
2234
@dataclass(frozen=True)
2335
class GeocodeResult:
2436
display_name: str
@@ -59,7 +71,7 @@ def _google_country_component() -> str | None:
5971

6072
def _google_geocode_address(query: str, *, limit: int) -> list[GeocodeResult]:
6173
if not GOOGLE_MAPS_API_KEY:
62-
return []
74+
raise GeocodingConfigError("GOOGLE_MAPS_API_KEY is not set")
6375
q = query.strip()
6476
if not q:
6577
return []
@@ -78,9 +90,12 @@ def _google_geocode_address(query: str, *, limit: int) -> list[GeocodeResult]:
7890
data = res.json()
7991

8092
if not isinstance(data, dict):
81-
return []
82-
if data.get("status") != "OK":
83-
return []
93+
raise GeocodingProviderError("Google Geocoding returned an invalid response")
94+
status = data.get("status")
95+
if status != "OK":
96+
error_message = data.get("error_message")
97+
extra = f": {error_message}" if isinstance(error_message, str) else ""
98+
raise GeocodingProviderError(f"Google Geocoding failed with status {status}{extra}")
8499
results = data.get("results")
85100
if not isinstance(results, list):
86101
return []
@@ -110,7 +125,7 @@ def _google_geocode_address(query: str, *, limit: int) -> list[GeocodeResult]:
110125

111126
def _google_reverse_geocode(lat: float, lng: float) -> ReverseGeocodeResult:
112127
if not GOOGLE_MAPS_API_KEY:
113-
return ReverseGeocodeResult(display_name=None, address=None)
128+
raise GeocodingConfigError("GOOGLE_MAPS_API_KEY is not set")
114129

115130
params: dict[str, str] = {"latlng": f"{lat},{lng}", "key": GOOGLE_MAPS_API_KEY}
116131
res = httpx.get(
@@ -122,9 +137,12 @@ def _google_reverse_geocode(lat: float, lng: float) -> ReverseGeocodeResult:
122137
data = res.json()
123138

124139
if not isinstance(data, dict):
125-
return ReverseGeocodeResult(display_name=None, address=None)
126-
if data.get("status") != "OK":
127-
return ReverseGeocodeResult(display_name=None, address=None)
140+
raise GeocodingProviderError("Google Geocoding returned an invalid response")
141+
status = data.get("status")
142+
if status != "OK":
143+
error_message = data.get("error_message")
144+
extra = f": {error_message}" if isinstance(error_message, str) else ""
145+
raise GeocodingProviderError(f"Google Geocoding failed with status {status}{extra}")
128146
results = data.get("results")
129147
if not isinstance(results, list) or not results:
130148
return ReverseGeocodeResult(display_name=None, address=None)

backend/app/main.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from .geocoding import (
1717
approx_street_from_address,
1818
geocode_address,
19+
GeocodingConfigError,
20+
GeocodingProviderError,
1921
reverse_geocode,
2022
rough_location_from_address,
2123
)
@@ -101,7 +103,7 @@ def upsert_listing(payload: ListingUpsert, db: DbDep) -> Listing:
101103
if candidates:
102104
existing.lat = existing.lat or candidates[0].lat
103105
existing.lng = existing.lng or candidates[0].lng
104-
except HTTPError:
106+
except (HTTPError, GeocodingConfigError, GeocodingProviderError):
105107
pass
106108

107109
if (
@@ -114,7 +116,7 @@ def upsert_listing(payload: ListingUpsert, db: DbDep) -> Listing:
114116
rough = rough_location_from_address(rev.address)
115117
if rough:
116118
existing.location_text = rough
117-
except HTTPError:
119+
except (HTTPError, GeocodingConfigError, GeocodingProviderError):
118120
pass
119121

120122
db.add(existing)
@@ -145,15 +147,15 @@ def upsert_listing(payload: ListingUpsert, db: DbDep) -> Listing:
145147
if candidates:
146148
listing.lat = listing.lat or candidates[0].lat
147149
listing.lng = listing.lng or candidates[0].lng
148-
except HTTPError:
150+
except (HTTPError, GeocodingConfigError, GeocodingProviderError):
149151
pass
150152
if listing.location_text is None and listing.lat is not None and listing.lng is not None:
151153
try:
152154
rev = reverse_geocode(listing.lat, listing.lng, zoom=10)
153155
rough = rough_location_from_address(rev.address)
154156
if rough:
155157
listing.location_text = rough
156-
except HTTPError:
158+
except (HTTPError, GeocodingConfigError, GeocodingProviderError):
157159
pass
158160
db.add(listing)
159161
db.commit()
@@ -184,12 +186,18 @@ def upsert_target(payload: TargetUpsert, db: DbDep) -> Target:
184186
lat = payload.lat
185187
lng = payload.lng
186188
address = payload.address.strip() if isinstance(payload.address, str) else None
189+
if address == "":
190+
address = None
187191

188192
if lat is None or lng is None:
189193
try:
190194
candidates = geocode_address(address or "", limit=1)
191195
except HTTPError as e:
192196
raise HTTPException(status_code=502, detail=str(e)) from e
197+
except GeocodingConfigError as e:
198+
raise HTTPException(status_code=500, detail=str(e)) from e
199+
except GeocodingProviderError as e:
200+
raise HTTPException(status_code=502, detail=str(e)) from e
193201
if not candidates:
194202
raise HTTPException(status_code=404, detail="Address not found")
195203
lat = candidates[0].lat
@@ -244,6 +252,10 @@ def api_geocode(
244252
results = geocode_address(query, limit=limit)
245253
except HTTPError as e:
246254
raise HTTPException(status_code=502, detail=str(e)) from e
255+
except GeocodingConfigError as e:
256+
raise HTTPException(status_code=500, detail=str(e)) from e
257+
except GeocodingProviderError as e:
258+
raise HTTPException(status_code=502, detail=str(e)) from e
247259
return [GeocodeResultOut(display_name=r.display_name, lat=r.lat, lng=r.lng) for r in results]
248260

249261

@@ -257,6 +269,10 @@ def api_reverse_geocode(
257269
rev = reverse_geocode(lat, lng, zoom=zoom)
258270
except HTTPError as e:
259271
raise HTTPException(status_code=502, detail=str(e)) from e
272+
except GeocodingConfigError as e:
273+
raise HTTPException(status_code=500, detail=str(e)) from e
274+
except GeocodingProviderError as e:
275+
raise HTTPException(status_code=502, detail=str(e)) from e
260276
return ReverseGeocodeOut(
261277
display_name=rev.display_name,
262278
rough_location=rough_location_from_address(rev.address),

backend/app/schemas.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,13 @@ def _validate_location(self) -> "TargetUpsert":
6464
has_lat = self.lat is not None
6565
has_lng = self.lng is not None
6666
has_address = self.address is not None and self.address.strip() != ""
67+
has_coords = has_lat and has_lng
6768

6869
if has_lat != has_lng:
6970
raise ValueError("lat and lng must be provided together")
70-
if not (has_address or (has_lat and has_lng)):
71+
if has_address and has_coords:
72+
raise ValueError("Provide either address, or both lat and lng (not both)")
73+
if not (has_address or has_coords):
7174
raise ValueError("Provide either address, or both lat and lng")
7275
return self
7376

backend/tests/test_schemas.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import sys
2+
from pathlib import Path
3+
4+
import pytest
5+
from pydantic import ValidationError
6+
7+
8+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9+
10+
11+
def test_target_upsert_rejects_neither_address_nor_coords() -> None:
12+
from app.schemas import TargetUpsert
13+
14+
with pytest.raises(ValidationError):
15+
TargetUpsert(name="Workplace")
16+
17+
18+
def test_target_upsert_rejects_both_address_and_coords() -> None:
19+
from app.schemas import TargetUpsert
20+
21+
with pytest.raises(ValidationError):
22+
TargetUpsert(
23+
name="Workplace",
24+
address="690 E Middlefield Rd, Mountain View, CA 94043",
25+
lat=37.4,
26+
lng=-122.1,
27+
)
28+
29+
30+
def test_target_upsert_accepts_address_only() -> None:
31+
from app.schemas import TargetUpsert
32+
33+
TargetUpsert(name="Workplace", address="690 E Middlefield Rd, Mountain View, CA 94043")
34+
35+
36+
def test_target_upsert_accepts_coords_only() -> None:
37+
from app.schemas import TargetUpsert
38+
39+
TargetUpsert(name="Workplace", lat=37.4, lng=-122.1)
40+

docs/GOOGLE_MAPS_APPROX_LOCATION.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ Response includes:
3535
The UI requests this **on demand** (we don’t persist “street” in the database).
3636

3737
## Using Google Geocoding API (recommended for Google-maps Airbnb pages)
38+
### Requirements
39+
In Google Cloud Console, you must:
40+
1. **Enable billing** for the project (Google Maps Platform).
41+
2. **Enable the “Geocoding API”** on the same project as your API key.
42+
43+
If you see an error like:
44+
`Google Geocoding failed with status REQUEST_DENIED: This API is not activated on your API project`
45+
it means the **Geocoding API is not enabled** (or billing/key restrictions are blocking it).
46+
3847
Set these env vars before starting the backend:
3948
```bash
4049
export GOOGLE_MAPS_API_KEY="YOUR_KEY"
@@ -53,11 +62,15 @@ source .venv/bin/activate
5362
uvicorn app.main:app --reload --port 8000
5463
```
5564

65+
### Common Google key setup notes
66+
- This project calls Google from the **backend** (server-to-server). If you restrict the key, prefer:
67+
- Restrict by **API**: allow only **Geocoding API**
68+
- Optionally restrict by **IP address** (for local dev, this can be inconvenient)
69+
5670
## Local dev env vars
5771
Backend env vars:
5872
- `GOOGLE_MAPS_API_KEY` (enables Google geocoding)
5973
- `GEOCODING_PROVIDER` (`google` or `nominatim`)
6074
- `GEOCODING_COUNTRY_CODES` (default `us`)
6175
- `ENABLE_GEOCODING` (default `1`)
6276
- `ENABLE_LISTING_GEOCODE_FALLBACK` (default `0`, if `1` will approximate coords from city/region text)
63-

frontend/src/App.css

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,21 +220,47 @@
220220
}
221221

222222
.marker {
223-
width: 12px;
224-
height: 12px;
223+
width: 26px;
224+
height: 26px;
225225
border-radius: 999px;
226-
border: 2px solid #ffffff;
227-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
226+
border: 2px solid rgba(255, 255, 255, 0.95);
227+
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.25);
228+
display: grid;
229+
place-items: center;
230+
color: #ffffff;
231+
background: #64748b;
228232
}
229233

230234
.marker.target {
235+
width: 34px;
236+
height: 34px;
231237
background: #ef4444;
238+
box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.25), 0 6px 14px rgba(15, 23, 42, 0.25);
232239
}
233240

234241
.marker.listing {
235242
background: #2563eb;
236243
}
237244

245+
.marker.selected {
246+
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.25), 0 6px 14px rgba(15, 23, 42, 0.25);
247+
}
248+
249+
.markerIcon {
250+
width: 14px;
251+
height: 14px;
252+
transition: transform 120ms ease;
253+
}
254+
255+
.marker.target .markerIcon {
256+
width: 18px;
257+
height: 18px;
258+
}
259+
260+
.marker.selected .markerIcon {
261+
transform: scale(1.15);
262+
}
263+
238264
.landing {
239265
min-height: 100vh;
240266
display: flex;

frontend/src/MapView.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ const OSM_RASTER_STYLE: StyleSpecification = {
3939
layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
4040
}
4141

42+
const HOUSE_ICON_SVG = `
43+
<svg class="markerIcon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
44+
<path
45+
d="M3 11.2L12 4l9 7.2V20a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1v-8.8Z"
46+
stroke="currentColor"
47+
stroke-width="2"
48+
stroke-linejoin="round"
49+
/>
50+
</svg>
51+
`.trim()
52+
53+
const LAPTOP_ICON_SVG = `
54+
<svg class="markerIcon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
55+
<path
56+
d="M6 5h12a2 2 0 0 1 2 2v8H4V7a2 2 0 0 1 2-2Z"
57+
stroke="currentColor"
58+
stroke-width="2"
59+
stroke-linejoin="round"
60+
/>
61+
<path
62+
d="M2 17h20v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1Z"
63+
stroke="currentColor"
64+
stroke-width="2"
65+
stroke-linejoin="round"
66+
/>
67+
</svg>
68+
`.trim()
69+
4270
function isWithinUsBounds(lat: number, lng: number): boolean {
4371
return (
4472
lat >= US_BOUNDS[0][1] &&
@@ -127,7 +155,9 @@ export default function MapView({
127155
// create markers
128156
for (const p of points) {
129157
const el = document.createElement('div')
130-
el.className = `marker ${p.kind}`
158+
const isSelected = p.kind === 'listing' && p.id === selectedListingId
159+
el.className = `marker ${p.kind}${isSelected ? ' selected' : ''}`
160+
el.innerHTML = p.kind === 'target' ? LAPTOP_ICON_SVG : HOUSE_ICON_SVG
131161
el.style.cursor = p.kind === 'listing' && !isPickingTarget ? 'pointer' : 'default'
132162

133163
const marker = new maplibregl.Marker({ element: el })
@@ -152,7 +182,7 @@ export default function MapView({
152182
map.fitBounds(bounds, { padding: 48, maxZoom: 13, duration: 450 })
153183
hasFitRef.current = fitKey
154184
}
155-
}, [fitKey, isPickingTarget, onSelectListingId, points])
185+
}, [fitKey, isPickingTarget, onSelectListingId, points, selectedListingId])
156186

157187
useEffect(() => {
158188
const map = mapRef.current

0 commit comments

Comments
 (0)