@osm-editor-kit/osm-coverage 0.1.0-alpha.0 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,17 @@
1
1
  # `@osm-editor-kit/osm-coverage`
2
2
 
3
- **Status:** First npm **alpha** (`0.1.0-alpha.0`). Publish with `bun run packages:release -- --publish-only` after `npm login`.
3
+ > [!NOTE]
4
+ > This package is an **alpha** release and still under active development. APIs may change; install with the npm `alpha` dist-tag.
4
5
 
5
6
  ## What it does
6
7
 
7
- TanStack Query–backed OSM viewport coverage for map editors: incremental downloads, merged `ParsedOsmData` graphs, coverage polygons, and fetch-history GeoJSON. Includes zoom-aware missing-area detection (`computeMissingFetchRequests`, `unionIntoCoverage`), Overpass URL helpers (`boundsToOverpassBbox`, `buildOverpassInterpreterUrl`, `overpassDeUrl`, `overpassVkUrl`), and an `OsmDataSource` enum for download backends.
8
-
9
- The `@osm-editor-kit/osm-coverage/dev-osm-map-fixture` subpath is **editor-dev only** — local OSM Map API fixture handler, bbox filtering, and fixture index loader.
8
+ - Download OSM data for the current map viewport (and only the parts you have not covered yet)
9
+ - Keep a session graph of ways/nodes/relations as the map moves and pans
10
+ - Track covered areas as polygons so refetches skip what you already have
11
+ - Record fetch history as GeoJSON for debugging and UI
12
+ - Build Overpass query URLs for common interpreters
13
+ - Optional durable session storage (`storage` adapter) for graph + coverage across reloads
14
+ - Optional editor-dev fixture loader via `@osm-editor-kit/osm-coverage/dev-osm-map-fixture` (not for production apps; not on the main export)
10
15
 
11
16
  ## Usage
12
17
 
@@ -17,13 +22,14 @@ import { createOsmCoverageApi, OsmDataSource } from '@osm-editor-kit/osm-coverag
17
22
  const api = createOsmCoverageApi<{ source: OsmDataSource }>({
18
23
  getSessionKey: ({ source }) => ['osm', source] as const,
19
24
  minZoom: 14,
20
- getDownloadUrl: (bounds, { source }) =>
21
- /* build Overpass or Map API URL */ '',
25
+ getDownloadUrl: (bounds, { source }) => /* build Overpass or Map API URL */ '',
26
+ // optional: storage: { load, save, clear }
22
27
  })
23
28
 
24
29
  const queryClient = new QueryClient()
25
30
  const params = { source: OsmDataSource.OverpassVk }
26
31
 
32
+ await api.restoreSession(queryClient, params)
27
33
  await api.ensureCoverage(queryClient, {
28
34
  bounds: { south: 52.47, west: 13.44, north: 52.48, east: 13.45 },
29
35
  zoom: 18,
@@ -32,5 +38,5 @@ await api.ensureCoverage(queryClient, {
32
38
  })
33
39
 
34
40
  const data = queryClient.getQueryData(api.sessionKey(params))
35
- // { graph, coverage, fetchHistory }
41
+ // { graph, coverage, fetchHistory, savedAt }
36
42
  ```
@@ -31,6 +31,11 @@ export declare function isValidBounds(bounds: MapBounds): boolean;
31
31
  export declare function boundsArea(bounds: MapBounds): number;
32
32
  export declare function bufferBoundsInViewport(request: MapBounds, viewport: MapBounds, zoom: number, mapSizePx: MapSizePx): MapBounds;
33
33
  export declare function clampBoundsToViewport(request: MapBounds, viewport: MapBounds): MapBounds;
34
+ /** Lon/lat length of the zoom-aware seam overlap (same magnitude as fetch buffer). */
35
+ export declare function overlapDeltaAtZoom(viewport: MapBounds, zoom: number, mapSizePx: MapSizePx): {
36
+ lon: number;
37
+ lat: number;
38
+ };
34
39
  export declare function createViewportFetchRequest(viewport: MapBounds, zoom: number, mapSizePx: MapSizePx, kind: CoverageFetchKind): CoverageFetchRequest | null;
35
40
  export declare function computeMissingFetchRequests(viewport: MapBounds, coverage: Feature<Polygon | MultiPolygon> | null, zoom: number, mapSizePx: MapSizePx): CoverageFetchRequest[];
36
41
  export declare function unionIntoCoverage(coverage: Feature<Polygon | MultiPolygon> | null, requestBounds: MapBounds): Feature<Polygon | MultiPolygon>;
@@ -2,11 +2,16 @@ import { type MapBounds, type ParsedOsmData } from '@osm-editor-kit/osm-data';
2
2
  import { type QueryClient } from '@tanstack/react-query';
3
3
  import type { Feature, FeatureCollection, MultiPolygon, Polygon } from 'geojson';
4
4
  import { type CoverageFetchProps, type MapSizePx } from './coverage-geometry';
5
+ import type { OsmCoverageStorage } from './osm-coverage-storage';
5
6
  export type { CoverageFetchProps, MapSizePx } from './coverage-geometry';
7
+ export type { OsmCoveragePersisted, OsmCoverageStorage } from './osm-coverage-storage';
8
+ export { formatCoverageAgeHour, getCoverageSavedAt } from './osm-coverage-storage';
6
9
  export type OsmCoverageQueryData = {
7
10
  graph: ParsedOsmData;
8
11
  coverage: Feature<Polygon | MultiPolygon> | null;
9
12
  fetchHistory: FeatureCollection<Polygon, CoverageFetchProps>;
13
+ /** ISO timestamp of last durable save / successful network merge; null if never saved. */
14
+ savedAt: string | null;
10
15
  };
11
16
  export type CreateOsmCoverageApiOptions<TSessionParams> = {
12
17
  getSessionKey: (params: TSessionParams) => readonly unknown[];
@@ -15,22 +20,34 @@ export type CreateOsmCoverageApiOptions<TSessionParams> = {
15
20
  download?: (url: string) => Promise<ParsedOsmData>;
16
21
  /** When false, session queries stay enabled=false and ensureCoverage skips network. */
17
22
  isNetworkEnabled?: () => boolean;
23
+ /** Optional durable store for graph + coverage across reloads. */
24
+ storage?: OsmCoverageStorage;
25
+ /** Debounce durable writes after merges (ms). Default 500. */
26
+ storageSaveDebounceMs?: number;
18
27
  };
19
- export declare function createOsmCoverageApi<TSessionParams>({ getSessionKey, minZoom, getDownloadUrl, download, isNetworkEnabled, }: CreateOsmCoverageApiOptions<TSessionParams>): {
28
+ export declare function createOsmCoverageApi<TSessionParams>({ getSessionKey, minZoom, getDownloadUrl, download, isNetworkEnabled, storage, storageSaveDebounceMs, }: CreateOsmCoverageApiOptions<TSessionParams>): {
20
29
  emptyData: () => OsmCoverageQueryData;
21
30
  sessionKey: (params: TSessionParams) => readonly unknown[];
22
31
  coverageKey: (params: TSessionParams) => readonly [...unknown[], "coverage"];
23
- ensureCoverage: (queryClient: QueryClient, { bounds, zoom, mapSizePx, force, ...sessionParams }: {
32
+ ensureCoverage: (queryClient: QueryClient, { bounds, zoom, mapSizePx, force, skipRestore, clearPersistedOnForce, ...sessionParams }: {
24
33
  bounds: MapBounds;
25
34
  zoom: number;
26
35
  mapSizePx: MapSizePx;
27
36
  force?: boolean;
37
+ /** Skip durable hydrate for this call (e.g. prefer-fresh). */
38
+ skipRestore?: boolean;
39
+ /** When force=true, also wipe durable storage before refetch. */
40
+ clearPersistedOnForce?: boolean;
28
41
  } & TSessionParams) => Promise<{
29
42
  skipped: boolean;
30
43
  }>;
44
+ restoreSession: (queryClient: QueryClient, params: TSessionParams, options?: {
45
+ forceReload?: boolean;
46
+ }) => Promise<boolean>;
47
+ clearPersisted: (params: TSessionParams) => Promise<void>;
31
48
  createUseQuery: (useSessionParams: () => TSessionParams) => <TData = OsmCoverageQueryData>(options?: {
32
49
  select?: (data: OsmCoverageQueryData) => TData;
33
- }) => import("@tanstack/react-query").DefinedUseQueryResult<NoInfer<TData>, Error>;
50
+ }) => import("@tanstack/react-query").UseQueryResult<NoInfer<TData>, Error>;
34
51
  createUseIsFetching: (useSessionParams: () => TSessionParams) => () => boolean;
35
52
  createUseResetOnSessionChange: (useSessionParams: () => TSessionParams) => () => void;
36
53
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- export { DEV_OSM_MAP_FIXTURE_ROUTE, devOsmMapFixtureMapUrl, } from './dev-osm-map-fixture/dev-osm-map-fixture-url';
2
- export { createOsmCoverageApi, type OsmCoverageQueryData } from './create-osm-coverage-api';
1
+ export { createOsmCoverageApi, type OsmCoverageQueryData, type OsmCoveragePersisted, type OsmCoverageStorage, formatCoverageAgeHour, getCoverageSavedAt, } from './create-osm-coverage-api';
3
2
  export { type CoverageFetchProps, type CoverageFetchKind, type MapSizePx, boundsToPolygon, bufferPxAtZoom, computeMissingFetchRequests, unionIntoCoverage, } from './coverage-geometry';
4
3
  export { OsmDataSource } from './osm-data-source';
5
4
  export { boundsToOverpassBbox, buildOverpassInterpreterUrl } from './overpass-url';
package/dist/index.js CHANGED
@@ -10,13 +10,7 @@ import { featureCollection } from '@turf/helpers';
10
10
  import intersect from '@turf/intersect';
11
11
  import union from '@turf/union';
12
12
 
13
- // src/dev-osm-map-fixture/dev-osm-map-fixture-url.ts
14
- var DEV_OSM_MAP_FIXTURE_ROUTE = "dev-osm-map-fixture";
15
- function devOsmMapFixtureMapUrl(baseUrl, bounds) {
16
- const base = baseUrl.replace(/\/$/, "");
17
- const bbox2 = [bounds.west, bounds.south, bounds.east, bounds.north].join(",");
18
- return `${base}/${DEV_OSM_MAP_FIXTURE_ROUTE}/api/0.6/map?bbox=${bbox2}`;
19
- }
13
+ // src/create-osm-coverage-api.ts
20
14
  var BUFFER_PX_AT_Z13 = 200;
21
15
  var MIN_MISSING_AREA_RATIO = 5e-3;
22
16
  var FULL_FETCH_RATIO = 0.85;
@@ -54,19 +48,15 @@ function bufferBoundsInViewport(request, viewport, zoom, mapSizePx) {
54
48
  north: request.north + latBuffer
55
49
  };
56
50
  }
57
- function clampBoundsToViewport(request, viewport) {
51
+ function overlapDeltaAtZoom(viewport, zoom, mapSizePx) {
52
+ const px = bufferPxAtZoom(zoom);
58
53
  return {
59
- west: Math.max(request.west, viewport.west),
60
- south: Math.max(request.south, viewport.south),
61
- east: Math.min(request.east, viewport.east),
62
- north: Math.min(request.north, viewport.north)
54
+ lon: px * (viewport.east - viewport.west) / mapSizePx.width,
55
+ lat: px * (viewport.north - viewport.south) / mapSizePx.height
63
56
  };
64
57
  }
65
58
  function prepareFetchBounds(request, viewport, zoom, mapSizePx) {
66
- const buffered = clampBoundsToViewport(
67
- bufferBoundsInViewport(request, viewport, zoom, mapSizePx),
68
- viewport
69
- );
59
+ const buffered = bufferBoundsInViewport(request, viewport, zoom, mapSizePx);
70
60
  if (!isValidBounds(buffered)) return null;
71
61
  const viewportArea = boundsArea(viewport);
72
62
  if (boundsArea(buffered) < MIN_MISSING_AREA_RATIO * viewportArea) return null;
@@ -80,27 +70,28 @@ function missingParts(viewport, coverage) {
80
70
  (feature) => feature.geometry.type === "Polygon"
81
71
  );
82
72
  }
83
- function stripCandidates(viewport, coverage, missingPart) {
73
+ function stripCandidates(viewport, coverage, missingPart, zoom, mapSizePx) {
84
74
  const viewportPoly = boundsToPolygon(viewport);
85
75
  const covered = intersect(featureCollection([viewportPoly, coverage]));
86
76
  if (!covered) return [];
87
77
  const coveredBbox = polygonToBounds(covered);
78
+ const { lon, lat } = overlapDeltaAtZoom(viewport, zoom, mapSizePx);
88
79
  const candidates = [
89
80
  {
90
81
  west: viewport.west,
91
82
  south: viewport.south,
92
- east: coveredBbox.west,
83
+ east: Math.min(viewport.east, coveredBbox.west + lon),
93
84
  north: viewport.north
94
85
  },
95
86
  {
96
- west: coveredBbox.east,
87
+ west: Math.max(viewport.west, coveredBbox.east - lon),
97
88
  south: viewport.south,
98
89
  east: viewport.east,
99
90
  north: viewport.north
100
91
  },
101
92
  {
102
93
  west: viewport.west,
103
- south: coveredBbox.north,
94
+ south: Math.max(viewport.south, coveredBbox.north - lat),
104
95
  east: viewport.east,
105
96
  north: viewport.north
106
97
  },
@@ -108,7 +99,7 @@ function stripCandidates(viewport, coverage, missingPart) {
108
99
  west: viewport.west,
109
100
  south: viewport.south,
110
101
  east: viewport.east,
111
- north: coveredBbox.south
102
+ north: Math.min(viewport.north, coveredBbox.south + lat)
112
103
  }
113
104
  ];
114
105
  return candidates.filter((strip) => {
@@ -117,14 +108,14 @@ function stripCandidates(viewport, coverage, missingPart) {
117
108
  return overlap !== null && area(overlap) > 0;
118
109
  });
119
110
  }
120
- function chooseBoundsForMissingPart(viewport, coverage, missingPart) {
111
+ function chooseBoundsForMissingPart(viewport, coverage, missingPart, zoom, mapSizePx) {
121
112
  const partArea = area(missingPart);
122
113
  const partBbox = polygonToBounds(missingPart);
123
114
  const fillRatio = partArea / area(boundsToPolygon(partBbox));
124
115
  if (fillRatio >= HIGH_FILL_RATIO) {
125
116
  return [partBbox];
126
117
  }
127
- const strips = stripCandidates(viewport, coverage, missingPart);
118
+ const strips = stripCandidates(viewport, coverage, missingPart, zoom, mapSizePx);
128
119
  const stripAreas = strips.map((strip) => ({ bounds: strip, area: boundsArea(strip) })).sort((a, b) => b.area - a.area);
129
120
  const stripTotalArea = stripAreas.reduce((sum, strip) => sum + strip.area, 0);
130
121
  const bboxArea = boundsArea(partBbox);
@@ -149,7 +140,7 @@ function computeMissingFetchRequests(viewport, coverage, zoom, mapSizePx) {
149
140
  );
150
141
  if (significantParts.length === 0) return [];
151
142
  const rawBounds = significantParts.flatMap(
152
- (part) => chooseBoundsForMissingPart(viewport, coverage, part)
143
+ (part) => chooseBoundsForMissingPart(viewport, coverage, part, zoom, mapSizePx)
153
144
  );
154
145
  let requests = rawBounds.map((request) => {
155
146
  const prepared = prepareFetchBounds(request, viewport, zoom, mapSizePx);
@@ -166,6 +157,15 @@ function computeMissingFetchRequests(viewport, coverage, zoom, mapSizePx) {
166
157
  return full ? [full] : [];
167
158
  }
168
159
  requests = requests.slice(0, MAX_STRIP_REQUESTS);
160
+ let projected = coverage;
161
+ for (const request of requests) {
162
+ projected = unionIntoCoverage(projected, request.bounds);
163
+ }
164
+ const leftover = missingParts(viewport, projected).reduce((sum, part) => sum + area(part), 0);
165
+ if (leftover >= MIN_MISSING_AREA_RATIO * viewportArea) {
166
+ const full = createViewportFetchRequest(viewport, zoom, mapSizePx, "full");
167
+ return full ? [full] : [];
168
+ }
169
169
  return requests.map((request) => ({
170
170
  bounds: request.bounds,
171
171
  kind: request.kind
@@ -209,29 +209,112 @@ function appendFetchHistory(history, groupId, fetchedAt, requests) {
209
209
  });
210
210
  }
211
211
 
212
+ // src/osm-coverage-storage.ts
213
+ function getCoverageSavedAt(savedAt) {
214
+ if (!savedAt) return null;
215
+ const parsed = Date.parse(savedAt);
216
+ return Number.isNaN(parsed) ? null : savedAt;
217
+ }
218
+ function formatCoverageAgeHour(savedAt, now = /* @__PURE__ */ new Date()) {
219
+ const iso = getCoverageSavedAt(savedAt);
220
+ if (!iso) return null;
221
+ const saved = new Date(iso);
222
+ saved.setMinutes(0, 0, 0);
223
+ const hour = String(saved.getHours()).padStart(2, "0");
224
+ const sameDay = saved.getFullYear() === now.getFullYear() && saved.getMonth() === now.getMonth() && saved.getDate() === now.getDate();
225
+ if (sameDay) {
226
+ return `${hour}:00`;
227
+ }
228
+ const y = saved.getFullYear();
229
+ const m = String(saved.getMonth() + 1).padStart(2, "0");
230
+ const d = String(saved.getDate()).padStart(2, "0");
231
+ return `${y}-${m}-${d} ${hour}:00`;
232
+ }
233
+
212
234
  // src/create-osm-coverage-api.ts
235
+ var DEFAULT_STORAGE_SAVE_DEBOUNCE_MS = 500;
213
236
  function createOsmCoverageApi({
214
237
  getSessionKey,
215
238
  minZoom,
216
239
  getDownloadUrl,
217
240
  download = downloadOsmData,
218
- isNetworkEnabled
241
+ isNetworkEnabled,
242
+ storage,
243
+ storageSaveDebounceMs = DEFAULT_STORAGE_SAVE_DEBOUNCE_MS
219
244
  }) {
220
245
  const getCoverageKey = (params) => [...getSessionKey(params), "coverage"];
221
246
  const networkEnabled = () => isNetworkEnabled?.() ?? true;
222
247
  const coverageChains = /* @__PURE__ */ new Map();
248
+ const hydratedSessions = /* @__PURE__ */ new Set();
249
+ const saveTimers = /* @__PURE__ */ new Map();
223
250
  function emptyData() {
224
251
  return {
225
252
  graph: emptyParsedOsmData(),
226
253
  coverage: null,
227
- fetchHistory: emptyFetchHistory()
254
+ fetchHistory: emptyFetchHistory(),
255
+ savedAt: null
228
256
  };
229
257
  }
258
+ function scheduleSave(sessionKey, data) {
259
+ if (!storage || !data.coverage) return;
260
+ const chainKey = JSON.stringify(sessionKey);
261
+ const previous = saveTimers.get(chainKey);
262
+ if (previous) clearTimeout(previous);
263
+ const persisted = {
264
+ graph: data.graph,
265
+ coverage: data.coverage,
266
+ savedAt: data.savedAt ?? (/* @__PURE__ */ new Date()).toISOString()
267
+ };
268
+ saveTimers.set(
269
+ chainKey,
270
+ setTimeout(() => {
271
+ saveTimers.delete(chainKey);
272
+ void storage.save(sessionKey, persisted).catch((error) => {
273
+ console.error("Failed to persist OSM coverage session", error);
274
+ });
275
+ }, storageSaveDebounceMs)
276
+ );
277
+ }
278
+ async function restoreSession(queryClient, params, options) {
279
+ if (!storage) return false;
280
+ const sessionKey = getSessionKey(params);
281
+ const chainKey = JSON.stringify(sessionKey);
282
+ if (options?.forceReload) {
283
+ hydratedSessions.delete(chainKey);
284
+ }
285
+ if (hydratedSessions.has(chainKey)) return false;
286
+ hydratedSessions.add(chainKey);
287
+ const loaded = await storage.load(sessionKey);
288
+ if (!loaded?.coverage) return false;
289
+ const current = queryClient.getQueryData(sessionKey);
290
+ if (current?.coverage) return false;
291
+ queryClient.setQueryData(sessionKey, {
292
+ graph: loaded.graph,
293
+ coverage: loaded.coverage,
294
+ fetchHistory: emptyFetchHistory(),
295
+ savedAt: loaded.savedAt
296
+ });
297
+ return true;
298
+ }
299
+ async function clearPersisted(params) {
300
+ if (!storage) return;
301
+ const sessionKey = getSessionKey(params);
302
+ const chainKey = JSON.stringify(sessionKey);
303
+ const pending = saveTimers.get(chainKey);
304
+ if (pending) {
305
+ clearTimeout(pending);
306
+ saveTimers.delete(chainKey);
307
+ }
308
+ hydratedSessions.delete(chainKey);
309
+ await storage.clear(sessionKey);
310
+ }
230
311
  async function ensureCoverage(queryClient, {
231
312
  bounds,
232
313
  zoom,
233
314
  mapSizePx,
234
315
  force = false,
316
+ skipRestore = false,
317
+ clearPersistedOnForce = false,
235
318
  ...sessionParams
236
319
  }) {
237
320
  if (!networkEnabled()) {
@@ -242,6 +325,12 @@ function createOsmCoverageApi({
242
325
  const coverageKey = getCoverageKey(params);
243
326
  const chainKey = JSON.stringify(sessionKey);
244
327
  const run = async () => {
328
+ if (!skipRestore && !force) {
329
+ await restoreSession(queryClient, params);
330
+ }
331
+ if (force && clearPersistedOnForce) {
332
+ await clearPersisted(params);
333
+ }
245
334
  const requestKey = [...coverageKey, crypto.randomUUID()];
246
335
  return queryClient.fetchQuery({
247
336
  queryKey: requestKey,
@@ -274,7 +363,14 @@ function createOsmCoverageApi({
274
363
  for (const newGraph of fetchedGraphs) {
275
364
  graph = mergeParsedOsm(graph, newGraph);
276
365
  }
277
- return { graph, coverage, fetchHistory };
366
+ const next2 = {
367
+ graph,
368
+ coverage,
369
+ fetchHistory,
370
+ savedAt: fetchedAt
371
+ };
372
+ scheduleSave(sessionKey, next2);
373
+ return next2;
278
374
  });
279
375
  return { skipped: false };
280
376
  },
@@ -299,8 +395,11 @@ function createOsmCoverageApi({
299
395
  return useQuery({
300
396
  queryKey: getSessionKey(params),
301
397
  queryFn: () => emptyData(),
302
- initialData: emptyData(),
398
+ // Placeholder (not initialData) so durable restore via setQueryData is not fought
399
+ // by a permanently seeded empty session.
400
+ placeholderData: emptyData(),
303
401
  staleTime: Number.POSITIVE_INFINITY,
402
+ gcTime: Number.POSITIVE_INFINITY,
304
403
  enabled: networkEnabled(),
305
404
  select: options?.select
306
405
  });
@@ -332,6 +431,8 @@ function createOsmCoverageApi({
332
431
  sessionKey: getSessionKey,
333
432
  coverageKey: getCoverageKey,
334
433
  ensureCoverage,
434
+ restoreSession,
435
+ clearPersisted,
335
436
  createUseQuery,
336
437
  createUseIsFetching,
337
438
  createUseResetOnSessionChange
@@ -358,4 +459,4 @@ function buildOverpassInterpreterUrl(server, query) {
358
459
  var overpassDeUrl = "https://overpass-api.de/api/interpreter?data=";
359
460
  var overpassVkUrl = "https://maps.mail.ru/osm/tools/overpass/api/interpreter?data=";
360
461
 
361
- export { DEV_OSM_MAP_FIXTURE_ROUTE, OsmDataSource, boundsToOverpassBbox, boundsToPolygon, bufferPxAtZoom, buildOverpassInterpreterUrl, computeMissingFetchRequests, createOsmCoverageApi, devOsmMapFixtureMapUrl, overpassDeUrl, overpassVkUrl, unionIntoCoverage };
462
+ export { OsmDataSource, boundsToOverpassBbox, boundsToPolygon, bufferPxAtZoom, buildOverpassInterpreterUrl, computeMissingFetchRequests, createOsmCoverageApi, formatCoverageAgeHour, getCoverageSavedAt, overpassDeUrl, overpassVkUrl, unionIntoCoverage };
@@ -0,0 +1,20 @@
1
+ import type { ParsedOsmData } from '@osm-editor-kit/osm-data';
2
+ import type { Feature, MultiPolygon, Polygon } from 'geojson';
3
+ /** Durable session blob — fetchHistory stays memory-only. */
4
+ export type OsmCoveragePersisted = {
5
+ graph: ParsedOsmData;
6
+ coverage: Feature<Polygon | MultiPolygon> | null;
7
+ savedAt: string;
8
+ };
9
+ export type OsmCoverageStorage = {
10
+ load(sessionKey: readonly unknown[]): Promise<OsmCoveragePersisted | null>;
11
+ save(sessionKey: readonly unknown[], data: OsmCoveragePersisted): Promise<void>;
12
+ clear(sessionKey: readonly unknown[]): Promise<void>;
13
+ };
14
+ /** ISO `savedAt` from session query data, or null when never persisted/fetched. */
15
+ export declare function getCoverageSavedAt(savedAt: string | null | undefined): string | null;
16
+ /**
17
+ * Round `savedAt` down to the hour for UI (“OSM data from 14:00”).
18
+ * Returns null when missing/invalid.
19
+ */
20
+ export declare function formatCoverageAgeHour(savedAt: string | null | undefined, now?: Date): string | null;
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@osm-editor-kit/osm-coverage",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "Incremental OSM coverage fetching for map viewports with TanStack Query session state.",
5
+ "homepage": "https://github.com/osmberlin/street-space-editor/tree/main/packages/osm-coverage",
5
6
  "license": "MIT",
6
7
  "author": "Tobias Jordans",
7
8
  "repository": {
@@ -9,11 +10,11 @@
9
10
  "url": "git+https://github.com/osmberlin/street-space-editor.git",
10
11
  "directory": "packages/osm-coverage"
11
12
  },
12
- "type": "module",
13
- "sideEffects": false,
14
13
  "files": [
15
14
  "dist"
16
15
  ],
16
+ "type": "module",
17
+ "sideEffects": false,
17
18
  "exports": {
18
19
  ".": {
19
20
  "types": "./dist/index.d.ts",
@@ -35,7 +36,7 @@
35
36
  "postpack": "bun ../../scripts/package-exports-restore.ts"
36
37
  },
37
38
  "dependencies": {
38
- "@osm-editor-kit/osm-data": "workspace:*",
39
+ "@osm-editor-kit/osm-data": "^0.1.0-alpha.1",
39
40
  "@tanstack/react-query": "^5.85.5",
40
41
  "@turf/area": "^7.3.5",
41
42
  "@turf/bbox": "^7.3.5",