@happyvertical/smrt-places 0.30.0

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.
@@ -0,0 +1,606 @@
1
+ import { Asset } from '@happyvertical/smrt-assets';
2
+ import { HierarchyView } from '@happyvertical/smrt-core';
3
+ import { Location } from '@happyvertical/geo';
4
+ import { SmrtCollection } from '@happyvertical/smrt-core';
5
+ import { SmrtHierarchical } from '@happyvertical/smrt-core';
6
+ import { SmrtJunction } from '@happyvertical/smrt-core';
7
+ import { SmrtObject } from '@happyvertical/smrt-core';
8
+ import { SmrtObjectOptions } from '@happyvertical/smrt-core';
9
+
10
+ /**
11
+ * Check if two coordinates are within a threshold distance
12
+ *
13
+ * @param lat1 - First latitude
14
+ * @param lng1 - First longitude
15
+ * @param lat2 - Second latitude
16
+ * @param lng2 - Second longitude
17
+ * @param thresholdKm - Distance threshold in kilometers
18
+ * @returns True if coordinates are within threshold
19
+ */
20
+ export declare function areCoordinatesNear(lat1: number, lng1: number, lat2: number, lng2: number, thresholdKm?: number): boolean;
21
+
22
+ /**
23
+ * Calculate distance between two coordinates using Haversine formula
24
+ *
25
+ * @param lat1 - First latitude
26
+ * @param lng1 - First longitude
27
+ * @param lat2 - Second latitude
28
+ * @param lng2 - Second longitude
29
+ * @returns Distance in kilometers
30
+ */
31
+ export declare function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number;
32
+
33
+ /**
34
+ * Options for `PlaceCollection.discoverNearby`. Mirrors the inputs to
35
+ * `@happyvertical/geo`'s `findPoisNear` plus a couple of persistence knobs
36
+ * (PlaceType override, parent linkage) to match `lookupOrCreate`'s shape.
37
+ */
38
+ export declare interface DiscoverNearbyOptions {
39
+ /** Which geo provider to use. Defaults to openstreetmap. */
40
+ geoProvider?: 'google' | 'openstreetmap';
41
+ /**
42
+ * POI category filter, forwarded to the provider. See
43
+ * `@happyvertical/geo`'s `PoiSearchOptions.types` for provider-specific
44
+ * interpretation.
45
+ */
46
+ types?: string[];
47
+ /** Free-text keyword filter (provider-dependent; Google only). */
48
+ keyword?: string;
49
+ /** Max results per provider call. Default 20. */
50
+ limit?: number;
51
+ /** Preferred language for place names (Google only). */
52
+ language?: string;
53
+ /** Override the PlaceType slug assigned to created rows. */
54
+ typeSlug?: string;
55
+ /** Set parent place on created rows. */
56
+ parentId?: string | null;
57
+ }
58
+
59
+ /**
60
+ * Format coordinates as string
61
+ *
62
+ * @param latitude - Latitude value
63
+ * @param longitude - Longitude value
64
+ * @param precision - Number of decimal places (default: 6)
65
+ * @returns Formatted coordinate string
66
+ */
67
+ export declare function formatCoordinates(latitude: number, longitude: number, precision?: number): string;
68
+
69
+ /**
70
+ * Generate a display name from address components
71
+ *
72
+ * @param components - Address components
73
+ * @returns Formatted display name
74
+ */
75
+ export declare function generateDisplayName(components: Partial<GeoData>): string;
76
+
77
+ /**
78
+ * Geographic data structure
79
+ * All fields optional to support both real-world and abstract places
80
+ */
81
+ export declare interface GeoData {
82
+ latitude: number | null;
83
+ longitude: number | null;
84
+ streetNumber?: string;
85
+ streetName?: string;
86
+ city?: string;
87
+ region?: string;
88
+ country?: string;
89
+ postalCode?: string;
90
+ countryCode?: string;
91
+ timezone?: string;
92
+ }
93
+
94
+ /**
95
+ * Convert Location from @happyvertical/geo to GeoData
96
+ *
97
+ * @param location - Location from geocoding
98
+ * @returns GeoData object
99
+ */
100
+ export declare function locationToGeoData(location: Location): GeoData;
101
+
102
+ /**
103
+ * Options for lookupOrCreate method
104
+ */
105
+ export declare interface LookupOrCreateOptions {
106
+ /**
107
+ * Which geo provider to use for lookups
108
+ */
109
+ geoProvider?: 'google' | 'openstreetmap';
110
+ /**
111
+ * Force a specific place type
112
+ */
113
+ typeSlug?: string;
114
+ /**
115
+ * Set parent place if known
116
+ */
117
+ parentId?: string | null;
118
+ /**
119
+ * Whether to create if not found (default: true)
120
+ */
121
+ createIfNotFound?: boolean;
122
+ /**
123
+ * Coordinates for reverse geocoding
124
+ */
125
+ coords?: {
126
+ lat: number;
127
+ lng: number;
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Map Location type from @happyvertical/geo to PlaceType slug
133
+ *
134
+ * @param locationType - Location type from geocoding provider
135
+ * @returns PlaceType slug
136
+ */
137
+ export declare function mapLocationTypeToPlaceType(locationType: string): string;
138
+
139
+ /**
140
+ * Normalize address components by trimming and removing empty values
141
+ *
142
+ * @param components - Address components
143
+ * @returns Normalized components
144
+ */
145
+ export declare function normalizeAddressComponents(components: Partial<GeoData>): Partial<GeoData>;
146
+
147
+ /**
148
+ * Parse coordinate string to lat/lng
149
+ *
150
+ * Supports formats:
151
+ * - "lat, lng"
152
+ * - "lat,lng"
153
+ * - "lat lng"
154
+ *
155
+ * @param coordString - Coordinate string
156
+ * @returns Object with lat and lng, or null if invalid
157
+ */
158
+ export declare function parseCoordinates(coordString: string): {
159
+ lat: number;
160
+ lng: number;
161
+ } | null;
162
+
163
+ export declare class Place extends SmrtHierarchical {
164
+ tenantId: string | null;
165
+ typeId: string;
166
+ name: string;
167
+ description: string;
168
+ latitude: number | null;
169
+ longitude: number | null;
170
+ streetNumber: string;
171
+ streetName: string;
172
+ city: string;
173
+ region: string;
174
+ country: string;
175
+ postalCode: string;
176
+ countryCode: string;
177
+ timezone: string;
178
+ externalId: string;
179
+ source: string;
180
+ metadata: string;
181
+ createdAt: Date;
182
+ updatedAt: Date;
183
+ constructor(options?: PlaceOptions);
184
+ /**
185
+ * Get geographic data for this place
186
+ *
187
+ * @returns GeoData object with all geo fields
188
+ */
189
+ getGeoData(): GeoData;
190
+ /**
191
+ * Check if this place has geographic coordinates
192
+ *
193
+ * @returns True if latitude and longitude are set
194
+ */
195
+ hasCoordinates(): boolean;
196
+ /**
197
+ * Get metadata as parsed object
198
+ *
199
+ * @returns Parsed metadata object or empty object if no metadata
200
+ */
201
+ getMetadata(): Record<string, any>;
202
+ /**
203
+ * Set metadata from object
204
+ *
205
+ * @param data - Metadata object to store
206
+ */
207
+ setMetadata(data: Record<string, any>): void;
208
+ /**
209
+ * Update metadata by merging with existing values
210
+ *
211
+ * @param updates - Partial metadata to merge
212
+ */
213
+ updateMetadata(updates: Record<string, any>): void;
214
+ /**
215
+ * Get the place type
216
+ *
217
+ * @returns PlaceType instance or null if not found
218
+ */
219
+ getType(): Promise<any>;
220
+ private getPlaceAssetCollection;
221
+ getAssets(relationship?: string): Promise<Asset[]>;
222
+ addAsset(asset: Asset, relationship?: string, sortOrder?: number): Promise<void>;
223
+ removeAsset(assetId: string, relationship?: string): Promise<void>;
224
+ }
225
+
226
+ export declare class PlaceAsset extends SmrtObject {
227
+ tenantId: string | null;
228
+ placeId: string;
229
+ assetId: string;
230
+ relationship: string;
231
+ sortOrder: number;
232
+ constructor(options?: PlaceAssetOptions);
233
+ }
234
+
235
+ export declare class PlaceAssetCollection extends SmrtJunction<PlaceAsset> {
236
+ static readonly _itemClass: typeof PlaceAsset;
237
+ protected leftField: string;
238
+ protected rightField: string;
239
+ private placeCollectionPromise;
240
+ private getPlaceCollection;
241
+ getAssets(placeId: string, relationship?: string): Promise<Asset[]>;
242
+ addAsset(placeId: string, asset: Asset, relationship?: string, sortOrder?: number): Promise<void>;
243
+ removeAsset(placeId: string, assetId: string, relationship?: string): Promise<void>;
244
+ }
245
+
246
+ export declare interface PlaceAssetOptions extends SmrtObjectOptions {
247
+ placeId?: string;
248
+ assetId?: string;
249
+ relationship?: string;
250
+ sortOrder?: number;
251
+ tenantId?: string | null;
252
+ }
253
+
254
+ export declare class PlaceCollection extends SmrtCollection<Place> {
255
+ static readonly _itemClass: typeof Place;
256
+ /**
257
+ * Look up a place by query or coordinates, creating it if not found
258
+ *
259
+ * This is the key method for organic database growth:
260
+ * 1. Search local database first
261
+ * 2. If not found, query @happyvertical/geo
262
+ * 3. Create place from geocoding result
263
+ * 4. Return place
264
+ *
265
+ * @param query - Address or location query string
266
+ * @param options - Lookup options (provider, type, parent, etc.)
267
+ * @returns Place instance
268
+ */
269
+ lookupOrCreate(query: string, options?: LookupOrCreateOptions): Promise<Place | null>;
270
+ /**
271
+ * Find place by coordinates (within small threshold)
272
+ *
273
+ * @param latitude - Latitude to search
274
+ * @param longitude - Longitude to search
275
+ * @param threshold - Max distance in degrees (default: 0.0001 ~11m)
276
+ * @returns Place instance or null
277
+ */
278
+ private findByCoordinates;
279
+ /**
280
+ * Find place by query text (matches name, city, region, country)
281
+ *
282
+ * @param query - Search query
283
+ * @returns Place instance or null
284
+ */
285
+ private findByQuery;
286
+ /**
287
+ * Geocode query or coordinates using @happyvertical/geo
288
+ *
289
+ * @param query - Address query
290
+ * @param coords - Optional coordinates for reverse geocoding
291
+ * @param provider - Geo provider to use
292
+ * @returns Array of Location results
293
+ */
294
+ private geocode;
295
+ /**
296
+ * Find a place by the provider's native id (Google place_id, OSM
297
+ * osm-node-N, etc.). Used as the primary idempotency key by
298
+ * `discoverNearby` so repeat POI searches don't create duplicate rows.
299
+ */
300
+ private findByExternalId;
301
+ /**
302
+ * Idempotent "find or create" for a geo Location. Keyed purely on the
303
+ * provider's externalId (Google place_id, osm-node-*, etc.), which is
304
+ * stable per POI across repeat searches. The older `lookupOrCreate`
305
+ * keeps its own fallback chain (name/address/coordinate matching) for
306
+ * address-style workflows where externalId isn't reliable.
307
+ *
308
+ * Returns `{ place, created }` so callers can distinguish fresh rows
309
+ * from reused ones without re-querying the table — this is how
310
+ * `resolveTrackPlaces` derives its `cacheHitCount` without a full
311
+ * scan per bucket.
312
+ */
313
+ private ensureFromLocation;
314
+ /**
315
+ * Create place from @happyvertical/geo Location data
316
+ *
317
+ * @param location - Location from geocoding
318
+ * @param typeSlug - Optional type slug override
319
+ * @param parentId - Optional parent place ID
320
+ * @returns Created Place instance
321
+ */
322
+ private createFromLocation;
323
+ /**
324
+ * Discover POIs near a coordinate and persist them as Place rows.
325
+ *
326
+ * Composes `@happyvertical/geo`'s `findPoisNear` with `ensureFromLocation`
327
+ * so every returned POI is either reused from the local DB (when the
328
+ * provider returns an id matching an existing Place `externalId`) or
329
+ * created fresh otherwise. The cache is effectively automatic because
330
+ * the provider's own place_id becomes the Place row's `externalId`, so
331
+ * calling `discoverNearby` twice for the same area on the same provider
332
+ * is a no-op after the first run when the provider returns stable ids.
333
+ *
334
+ * Requires a geo provider that implements `findPoisNear`. Throws a clear
335
+ * error otherwise so consumers can fall back to `lookupOrCreate` or
336
+ * switch providers.
337
+ */
338
+ discoverNearby(latitude: number, longitude: number, radiusMeters: number, options?: DiscoverNearbyOptions): Promise<Place[]>;
339
+ /**
340
+ * Internal variant of `discoverNearby` that exposes whether each
341
+ * returned Place was created during this call (`true`) or reused from
342
+ * an earlier call (`false`). Used by `resolveTrackPlaces` to classify
343
+ * buckets as cache hits without having to re-scan the Place table.
344
+ */
345
+ private discoverNearbyDetailed;
346
+ /**
347
+ * Resolve POIs along a GPS track (e.g. a video's per-frame path).
348
+ *
349
+ * Naively walking every point would hammer the provider with redundant
350
+ * requests — consecutive samples are usually within a few meters. This
351
+ * method buckets points into a `bucketMeters`-wide grid, calls
352
+ * `discoverNearby` once per distinct bucket, and throttles requests per
353
+ * `throttleMs` so free tiers (Overpass, Nominatim) stay inside their
354
+ * community rate limits without the caller having to manage a queue.
355
+ *
356
+ * The returned `places` are deduped across buckets by Place id, so a
357
+ * POI that falls within several overlapping search radii appears once.
358
+ */
359
+ resolveTrackPlaces(points: ReadonlyArray<TrackPoint>, options?: ResolveTrackPlacesOptions): Promise<TrackPlacesResult>;
360
+ /**
361
+ * Provider wiring (env keys, user-agent string, etc.) that `geocode`
362
+ * and `discoverNearby` share so the two code paths can't drift. Kept
363
+ * separate from the adapter call itself so tests and future providers
364
+ * can inspect or override the options before construction.
365
+ */
366
+ private getGeoAdapterOptions;
367
+ /**
368
+ * Build a geo adapter configured for this call. Single source of truth
369
+ * for provider wiring — `geocode` and `discoverNearby` both come through
370
+ * here.
371
+ */
372
+ private getGeoAdapter;
373
+ /**
374
+ * Get immediate children of a parent place
375
+ *
376
+ * @param parentId - The parent place ID
377
+ * @returns Array of child places
378
+ */
379
+ getChildren(parentId: string): Promise<Place[]>;
380
+ /**
381
+ * Get root places (no parent)
382
+ *
383
+ * @returns Array of root places
384
+ */
385
+ getRootPlaces(): Promise<Place[]>;
386
+ /**
387
+ * Get places by type
388
+ *
389
+ * @param typeSlug - PlaceType slug
390
+ * @returns Array of places of that type
391
+ */
392
+ getByType(typeSlug: string): Promise<Place[]>;
393
+ /**
394
+ * Get place hierarchy (all ancestors and descendants)
395
+ *
396
+ * @param placeId - The place ID
397
+ * @returns Object with ancestors, current place, and descendants
398
+ */
399
+ getHierarchy(placeId: string): Promise<HierarchyView<Place>>;
400
+ getAssets(placeId: string, relationship?: string): Promise<Asset[]>;
401
+ addAsset(placeId: string, asset: Asset, relationship?: string, sortOrder?: number): Promise<void>;
402
+ removeAsset(placeId: string, assetId: string, relationship?: string): Promise<void>;
403
+ /**
404
+ * Search places by proximity to coordinates
405
+ *
406
+ * @param latitude - Center latitude
407
+ * @param longitude - Center longitude
408
+ * @param radiusKm - Search radius in kilometers
409
+ * @returns Array of places within radius, sorted by distance
410
+ */
411
+ searchByProximity(latitude: number, longitude: number, radiusKm?: number): Promise<Place[]>;
412
+ /**
413
+ * Calculate distance between two coordinates using Haversine formula
414
+ *
415
+ * @param lat1 - First latitude
416
+ * @param lng1 - First longitude
417
+ * @param lat2 - Second latitude
418
+ * @param lng2 - Second longitude
419
+ * @returns Distance in kilometers
420
+ */
421
+ private calculateDistance;
422
+ /**
423
+ * Convert degrees to radians
424
+ */
425
+ private toRad;
426
+ /**
427
+ * Find all places belonging to a specific tenant
428
+ *
429
+ * @param tenantId - The tenant ID to filter by
430
+ * @returns Array of places for the tenant
431
+ */
432
+ findByTenant(tenantId: string): Promise<Place[]>;
433
+ /**
434
+ * Find all global places (not associated with any tenant)
435
+ *
436
+ * @returns Array of global places
437
+ */
438
+ findGlobal(): Promise<Place[]>;
439
+ /**
440
+ * Find places for a tenant including global places
441
+ *
442
+ * @param tenantId - The tenant ID to include
443
+ * @returns Array of tenant-specific and global places
444
+ */
445
+ findWithGlobals(tenantId: string): Promise<Place[]>;
446
+ }
447
+
448
+ /**
449
+ * Place hierarchy structure
450
+ */
451
+ export declare interface PlaceHierarchy {
452
+ ancestors: any[];
453
+ current: any;
454
+ descendants: any[];
455
+ }
456
+
457
+ /**
458
+ * Options for creating/updating a Place
459
+ */
460
+ export declare interface PlaceOptions extends SmrtObjectOptions {
461
+ id?: string;
462
+ tenantId?: string | null;
463
+ typeId?: string;
464
+ parentId?: string | null;
465
+ name?: string;
466
+ description?: string;
467
+ latitude?: number | null;
468
+ longitude?: number | null;
469
+ streetNumber?: string;
470
+ streetName?: string;
471
+ city?: string;
472
+ region?: string;
473
+ country?: string;
474
+ postalCode?: string;
475
+ countryCode?: string;
476
+ timezone?: string;
477
+ externalId?: string;
478
+ source?: string;
479
+ metadata?: Record<string, any> | string;
480
+ createdAt?: Date;
481
+ updatedAt?: Date;
482
+ }
483
+
484
+ export declare class PlaceType extends SmrtObject {
485
+ name: string;
486
+ description?: string;
487
+ createdAt: Date;
488
+ updatedAt: Date;
489
+ constructor(options?: PlaceTypeOptions);
490
+ /**
491
+ * Convenience method for slug-based lookup
492
+ *
493
+ * @param slug - The slug to search for
494
+ * @returns PlaceType instance or null if not found
495
+ */
496
+ static getBySlug(_slug: string): Promise<PlaceType | null>;
497
+ }
498
+
499
+ export declare class PlaceTypeCollection extends SmrtCollection<PlaceType> {
500
+ static readonly _itemClass: typeof PlaceType;
501
+ /**
502
+ * Get or create a place type by slug
503
+ *
504
+ * @param slug - PlaceType slug (e.g., 'city', 'building')
505
+ * @param name - Optional display name (defaults to capitalized slug)
506
+ * @returns PlaceType instance
507
+ */
508
+ getOrCreate(slug: string, name?: string): Promise<PlaceType>;
509
+ /**
510
+ * Get a place type by slug
511
+ *
512
+ * @param slug - PlaceType slug to search for
513
+ * @returns PlaceType instance or null if not found
514
+ */
515
+ getBySlug(slug: string): Promise<PlaceType | null>;
516
+ /**
517
+ * Initialize default place types
518
+ *
519
+ * Creates standard types if they don't exist:
520
+ * - country
521
+ * - region (state/province)
522
+ * - city
523
+ * - address
524
+ * - building
525
+ * - room
526
+ * - zone (for abstract/virtual places)
527
+ *
528
+ * @returns Array of created/existing place types
529
+ */
530
+ initializeDefaults(): Promise<PlaceType[]>;
531
+ }
532
+
533
+ /**
534
+ * Options for creating/updating a PlaceType
535
+ */
536
+ export declare interface PlaceTypeOptions extends SmrtObjectOptions {
537
+ id?: string;
538
+ slug?: string;
539
+ name?: string;
540
+ description?: string;
541
+ createdAt?: Date;
542
+ updatedAt?: Date;
543
+ }
544
+
545
+ /**
546
+ * Options for `PlaceCollection.resolveTrackPlaces`. Extends
547
+ * `DiscoverNearbyOptions` with per-track knobs that control how the track
548
+ * is bucketed + throttled before hitting the geo provider.
549
+ */
550
+ export declare interface ResolveTrackPlacesOptions extends DiscoverNearbyOptions {
551
+ /**
552
+ * Per-point POI search radius in meters. Default 50.
553
+ */
554
+ radiusMeters?: number;
555
+ /**
556
+ * Collapse points within this distance (m) into a single bucket so we
557
+ * don't re-query the same ~50m area dozens of times for a slow drive-by.
558
+ * Default 50.
559
+ */
560
+ bucketMeters?: number;
561
+ /**
562
+ * Minimum delay between provider requests. Default 1100ms — safely above
563
+ * OSM's 1 req/sec community limit. Google can usually handle faster; set
564
+ * to 100ms or less for paid tiers.
565
+ */
566
+ throttleMs?: number;
567
+ }
568
+
569
+ /**
570
+ * Result of `PlaceCollection.resolveTrackPlaces`. Tracks are typically
571
+ * long-running operations so the return payload includes counters that let
572
+ * callers surface progress or estimate cost without re-walking.
573
+ */
574
+ export declare interface TrackPlacesResult {
575
+ /** De-duplicated Place rows touched during resolution. */
576
+ places: any[];
577
+ /** Number of provider requests issued. */
578
+ requestCount: number;
579
+ /** Number of bucketed points that reused existing Place rows. */
580
+ cacheHitCount: number;
581
+ /** Number of distinct geographic buckets walked. */
582
+ bucketCount: number;
583
+ }
584
+
585
+ /**
586
+ * A single point along a track. Matches the `{ lat, lng }` convention used
587
+ * by `LookupOrCreateOptions.coords` so callers don't have to shuffle shapes.
588
+ */
589
+ export declare interface TrackPoint {
590
+ lat: number;
591
+ lng: number;
592
+ }
593
+
594
+ /**
595
+ * Validate geographic coordinates
596
+ *
597
+ * @param latitude - Latitude value
598
+ * @param longitude - Longitude value
599
+ * @returns Object with valid flag and optional error message
600
+ */
601
+ export declare function validateCoordinates(latitude: number, longitude: number): {
602
+ valid: boolean;
603
+ error?: string;
604
+ };
605
+
606
+ export { }