fix(search): support PLZ filter on home page

The home page dropped `plz` server-side, so /markets was called with
radius but no center (unfiltered) and the PLZ input rendered empty
after reload. +page.server.ts now reads plz, geocodes via /geocode,
and echoes plz back in searchParams for form rehydration.

Relaxes /geocode DTO + guard to accept PLZ without city — Nominatim
already supports postal-only lookups. URL lat/lon (GPS flow) take
priority over plz on tie-break; geocode failures fall through to no
geo-filter so the page always renders.
This commit is contained in:
2026-04-18 05:32:28 +02:00
parent df5b0563c9
commit 79001011df
4 changed files with 48 additions and 25 deletions

View File

@@ -395,7 +395,7 @@ type SubmitMarketRequest struct {
type GeocodeRequest struct {
Street string `json:"street"`
City string `json:"city" validate:"required"`
City string `json:"city"`
Zip string `json:"zip"`
Country string `json:"country"`
}

View File

@@ -28,8 +28,8 @@ type nominatimResult struct {
}
func (g *Geocoder) Geocode(ctx context.Context, street, city, zip, country string) (*float64, *float64, error) {
if city == "" {
return nil, nil, fmt.Errorf("city is required for geocoding")
if city == "" && zip == "" {
return nil, nil, fmt.Errorf("city or zip is required for geocoding")
}
// Respect Nominatim rate limit: max 1 req/sec

View File

@@ -2,10 +2,34 @@ import type { PageServerLoad } from './$types.js';
import { apiFetch, buildSearchQuery } from '$lib/api/client.js';
import type { MarketSummary, PaginationMeta } from '$lib/api/types.js';
export const load: PageServerLoad = async ({ url, fetch }) => {
const params: Record<string, string> = {};
type Coords = { lat?: string; lon?: string };
async function resolveCoords(
plz: string | null,
urlLat: string | null,
urlLon: string | null,
fetch: typeof globalThis.fetch
): Promise<Coords> {
if (urlLat && urlLon) return { lat: urlLat, lon: urlLon };
if (!plz) return {};
try {
const res = await apiFetch<{ latitude: number | null; longitude: number | null }>('/geocode', {
method: 'POST',
body: JSON.stringify({ city: '', zip: plz, country: 'DE' }),
fetch
});
const { latitude, longitude } = res.data;
if (latitude == null || longitude == null) return {};
return { lat: String(latitude), lon: String(longitude) };
} catch {
return {};
}
}
export const load: PageServerLoad = async ({ url, fetch }) => {
const q = url.searchParams.get('q');
const plz = url.searchParams.get('plz');
const lat = url.searchParams.get('lat');
const lon = url.searchParams.get('lon');
const radius = url.searchParams.get('radius');
@@ -14,9 +38,12 @@ export const load: PageServerLoad = async ({ url, fetch }) => {
const sort = url.searchParams.get('sort');
const page = url.searchParams.get('page');
const coords = await resolveCoords(plz, lat, lon, fetch);
const params: Record<string, string> = {};
if (q) params.q = q;
if (lat) params.lat = lat;
if (lon) params.lon = lon;
if (coords.lat) params.lat = coords.lat;
if (coords.lon) params.lon = coords.lon;
if (radius) params.radius = radius;
if (from) params.from = from;
if (to) params.to = to;
@@ -26,34 +53,29 @@ export const load: PageServerLoad = async ({ url, fetch }) => {
const query = buildSearchQuery(params);
const path = `/markets${query ? `?${query}` : ''}`;
const searchParams = {
q: q ?? '',
plz: plz ?? '',
lat: lat ? Number(lat) : undefined,
lon: lon ? Number(lon) : undefined,
radius: radius ? Number(radius) : 25,
from: from ?? '',
to: to ?? '',
sort: sort ?? ''
};
try {
const res = await apiFetch<MarketSummary[]>(path, { fetch });
return {
markets: res.data,
meta: res.meta as PaginationMeta,
searchParams: {
q: q ?? '',
lat: lat ? Number(lat) : undefined,
lon: lon ? Number(lon) : undefined,
radius: radius ? Number(radius) : 25,
from: from ?? '',
to: to ?? '',
sort: sort ?? ''
}
searchParams
};
} catch {
return {
markets: [] as MarketSummary[],
meta: { page: 1, per_page: 20, total: 0, total_pages: 0 } as PaginationMeta,
searchParams: {
q: q ?? '',
lat: lat ? Number(lat) : undefined,
lon: lon ? Number(lon) : undefined,
radius: radius ? Number(radius) : 25,
from: from ?? '',
to: to ?? '',
sort: sort ?? ''
}
searchParams
};
}
};

View File

@@ -50,6 +50,7 @@
<SearchForm
q={data.searchParams.q}
plz={data.searchParams.plz}
radius={data.searchParams.radius}
from={data.searchParams.from}
to={data.searchParams.to}