From 37b4b1d251a52034d07174593334df681623d5eb Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 20:52:09 +0530 Subject: [PATCH 1/3] Add location precision levels and place helpers --- apps/web/src/lib/geocode.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/web/src/lib/geocode.ts b/apps/web/src/lib/geocode.ts index 2fe1a5e..98ddaac 100644 --- a/apps/web/src/lib/geocode.ts +++ b/apps/web/src/lib/geocode.ts @@ -4,6 +4,32 @@ export interface PlaceName { countryIso2: string } +export type Place = { city: string; country: string } + +// How much of the resolved place to show. The offline dataset only knows city +// and country, so finer levels (neighborhood) aren't offered — see #39. +export type LocationDetail = 'country' | 'city' | 'cityCountry' + +export const LOCATION_DETAILS: { id: LocationDetail; label: string }[] = [ + { id: 'country', label: 'Country' }, + { id: 'city', label: 'City' }, + { id: 'cityCountry', label: 'City, Country' }, +] + +export function placeLabel(place: Place, detail: LocationDetail): string { + if (detail === 'country') return place.country + if (detail === 'cityCountry') return `${place.city}, ${place.country}` + return place.city +} + +/** True if `text` is any auto-generated location label for this place. */ +export function isAutoLocation(text: string, place: Place): boolean { + return ( + text === '' || + LOCATION_DETAILS.some(({ id }) => placeLabel(place, id) === text) + ) +} + // The cities dataset (~1.7 MB) is lazy-loaded on first use so it never bloats // the initial bundle. Everything runs locally — coordinates never leave the // device (see docs/decisions/0001-offline-reverse-geocoding.md). From 871a310c6e8c92b84e330fe0d49d6e0988a7bced Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 20:52:09 +0530 Subject: [PATCH 2/3] Use location precision helpers in stores --- apps/web/src/stores/photo-store.ts | 12 +++++------- apps/web/src/stores/settings-store.ts | 3 ++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/web/src/stores/photo-store.ts b/apps/web/src/stores/photo-store.ts index 8de6b4b..b8ce424 100644 --- a/apps/web/src/stores/photo-store.ts +++ b/apps/web/src/stores/photo-store.ts @@ -3,7 +3,7 @@ import { create } from 'zustand' import { type Crop, type Orientation, orientationFor } from '@/lib/crop' import { type DateFormat, formatCaptionDate, isAutoDate } from '@/lib/date' import { readExif } from '@/lib/exif' -import { reverseGeocode } from '@/lib/geocode' +import { isAutoLocation, placeLabel, reverseGeocode } from '@/lib/geocode' import { type CaptionField, type Photo, @@ -76,7 +76,7 @@ export const usePhotoStore = create((set) => { const place = await reverseGeocode(exif.latitude, exif.longitude) if (place) { next.place = { city: place.city, country: place.country } - next.captionTop = next.place[settings.captionLocation] + next.captionTop = placeLabel(next.place, settings.captionLocation) } } if ( @@ -150,11 +150,9 @@ export const usePhotoStore = create((set) => { set((state) => ({ photos: state.photos.map((p) => { if (!p.place) return p - const isAuto = - p.captionTop === '' || - p.captionTop === p.place.city || - p.captionTop === p.place.country - return isAuto ? { ...p, captionTop: p.place[mode] } : p + return isAutoLocation(p.captionTop, p.place) + ? { ...p, captionTop: placeLabel(p.place, mode) } + : p }), })), applyDateFormat: (format) => diff --git a/apps/web/src/stores/settings-store.ts b/apps/web/src/stores/settings-store.ts index d5e5c17..e1b5b27 100644 --- a/apps/web/src/stores/settings-store.ts +++ b/apps/web/src/stores/settings-store.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { type DateFormat, DEFAULT_DATE_FORMAT } from '@/lib/date' import { DEFAULT_CAPTION_FONT_ID } from '@/lib/fonts' +import { type LocationDetail } from '@/lib/geocode' import { DEFAULT_PAPER_SIZE_ID } from '@/lib/layout' export const MIN_FRAME_PADDING = 6 @@ -10,7 +11,7 @@ export const MAX_FRAME_PADDING = 28 export const MIN_PER_ROW = 2 export const MAX_PER_ROW = 5 -export type CaptionLocation = 'city' | 'country' +export type CaptionLocation = LocationDetail /** The persistable subset of settings (no action functions). */ export interface SettingsSnapshot { From 1db16c1fdf2afd005d5e4f638e2a6b6d356808ff Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Sun, 28 Jun 2026 20:52:09 +0530 Subject: [PATCH 3/3] Add location precision picker --- apps/web/src/components/caption-controls.tsx | 41 +++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/caption-controls.tsx b/apps/web/src/components/caption-controls.tsx index 933a318..918de7f 100644 --- a/apps/web/src/components/caption-controls.tsx +++ b/apps/web/src/components/caption-controls.tsx @@ -1,5 +1,5 @@ import { DATE_FORMATS, type DateFormat } from '@/lib/date' -import { cn } from '@/lib/utils' +import { LOCATION_DETAILS } from '@/lib/geocode' import { usePhotoStore } from '@/stores/photo-store' import { type CaptionLocation, @@ -33,26 +33,29 @@ export function CaptionControls() { return (
- + -
- {(['city', 'country'] as const).map((location) => ( - + +