@mailwoman/spatial 4.9.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,419 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import type google from "@googlemaps/google-maps-services-js"
8
+ import type { LatLng, LatLngLiteral } from "@googlemaps/google-maps-services-js"
9
+ import { tryParsingJSON } from "@mailwoman/core/objects"
10
+ import { convert as convertCoords } from "geo-coordinates-parser"
11
+ import { latLngToCell } from "h3-js"
12
+ import { type BBox2DLiteral, type BBox3DLiteral, GeoBoundingBox, isBBox } from "../bbox.js"
13
+ import { type H3Cell, shortenH3Cell } from "../h3/index.js"
14
+ import { type GeoObjectLiteral, GeometryType } from "../objects.js"
15
+ import {
16
+ type InternalPointCoordinates,
17
+ type Coordinates2D as Point2DCoordinates,
18
+ type Coordinates3D as Point3DCoordinates,
19
+ clampLatitude,
20
+ inferGeoJSONCoordOrder,
21
+ isCoordPairLiteral,
22
+ isGoogleMapsLatLngLiteral,
23
+ isInterpolatedCoordinates,
24
+ wrapLongitude,
25
+ } from "../position.js"
26
+
27
+ /**
28
+ * A JSON-serializeable single point geometry, such as a specific location, address, or longitude,
29
+ * latitude pair.
30
+ *
31
+ * ```js
32
+ * {
33
+ * "type": "Point",
34
+ * "coordinates": [100, 0]
35
+ * }
36
+ * ```
37
+ *
38
+ * @title Point Geometry
39
+ * @public
40
+ */
41
+ export interface PointLiteral extends GeoObjectLiteral {
42
+ /**
43
+ * Declares the type of GeoJSON object as a `Point` geometry.
44
+ */
45
+ type: "Point"
46
+ /**
47
+ * A pair of coordinates in the form of [longitude, latitude].
48
+ *
49
+ * @see {@linkcode Point2DCoordinates} for more information.
50
+ */
51
+ coordinates: Point2DCoordinates | Point3DCoordinates
52
+ }
53
+
54
+ /**
55
+ * Type-predicate to determine if the given input is a GeoJSON Point geometry.
56
+ */
57
+ export function isPointLiteral(input: PointLiteral | null | undefined | unknown): input is PointLiteral {
58
+ if (!input || typeof input !== "object") return false
59
+
60
+ if (!("type" in input)) return false
61
+ if (!("coordinates" in input)) return false
62
+ if (input.type !== GeometryType.Point) return false
63
+
64
+ return isCoordPairLiteral(input.coordinates)
65
+ }
66
+
67
+ /**
68
+ * Common interface for Browser Geolocation API coordinates.
69
+ *
70
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates MDN Web Docs}
71
+ */
72
+ export interface GeolocationCoordinatesLike {
73
+ latitude: number
74
+ longitude: number
75
+ altitude: number
76
+ }
77
+
78
+ /**
79
+ * Type-predicate to determine if the given input appears to be a {@linkcode GeolocationCoordinates}
80
+ * object.
81
+ */
82
+ export function isGeolocationCoordinatesLike(input: unknown): input is GeolocationCoordinatesLike {
83
+ if (!input || typeof input !== "object") return false
84
+
85
+ if (!("latitude" in input)) return false
86
+ if (!("longitude" in input)) return false
87
+ if (!("altitude" in input)) return false
88
+
89
+ return true
90
+ }
91
+
92
+ export type GeoPointInput =
93
+ | PointLiteral
94
+ | Point2DCoordinates
95
+ | Point3DCoordinates
96
+ | GeolocationCoordinatesLike
97
+ | InternalPointCoordinates
98
+ | LatLngLiteral
99
+ | LatLng
100
+
101
+ //#region GeoPoint
102
+
103
+ /**
104
+ * A single point geometry, such as a specific location, address, or longitude, latitude pair.
105
+ */
106
+ export class GeoPoint implements PointLiteral {
107
+ //#region Properties
108
+
109
+ /**
110
+ * Declares the type of GeoJSON object as a `Point` geometry.
111
+ */
112
+ readonly type = GeometryType.Point
113
+
114
+ /**
115
+ * The bounding box literal of the GeoPoint.
116
+ *
117
+ * @see {@linkcode GeoPoint.bbox} for the actual bounding box object.
118
+ * @see {@linkcode GeoBoundingBox} for creating a bounding box.
119
+ */
120
+ public boundingBox: GeoBoundingBox | null = null
121
+
122
+ /**
123
+ * The bounding box literal of the GeoPoint.
124
+ *
125
+ * @see {@linkcode BBox2DLiteral} for 2D bounding boxes.
126
+ * @see {@linkcode BBox3DLiteral} for 3D bounding boxes.
127
+ * @see {@linkcode GeoBoundingBox} for creating a bounding box.
128
+ */
129
+ public get bbox(): BBox2DLiteral | BBox3DLiteral | undefined {
130
+ return this.boundingBox?.toJSON()
131
+ }
132
+
133
+ public set bbox(bbox: BBox2DLiteral | BBox3DLiteral | undefined) {
134
+ this.boundingBox = bbox ? new GeoBoundingBox(bbox) : null
135
+ }
136
+
137
+ public get coordinates(): Point2DCoordinates | Point3DCoordinates {
138
+ if (this.is3D()) {
139
+ return [this.#longitude, this.#latitude, this.#altitude]
140
+ }
141
+
142
+ return [this.#longitude, this.#latitude]
143
+ }
144
+
145
+ public set coordinates(coords: Point2DCoordinates | Point3DCoordinates) {
146
+ const [longitude, latitude, altitude] = coords
147
+
148
+ this.#longitude = longitude
149
+ this.#latitude = latitude
150
+ this.#altitude = typeof altitude === "number" ? altitude : 0
151
+ }
152
+
153
+ #latitude: number = 0
154
+ #longitude: number = 0
155
+ #altitude: number = 0
156
+
157
+ /**
158
+ * The longitude of the point in degrees, i.e. the x-coordinate.
159
+ *
160
+ * Values outside the range will be wrapped around to the opposite side of the globe.
161
+ *
162
+ * @minimum -180
163
+ * @maximum 180
164
+ */
165
+ public get longitude(): number {
166
+ return this.#longitude
167
+ }
168
+
169
+ public set longitude(value: number) {
170
+ this.#longitude = wrapLongitude(value)
171
+ }
172
+
173
+ /**
174
+ * The latitude of the point in degrees, i.e. the y-coordinate.
175
+ *
176
+ * Values outside the range will be clamped to the poles.
177
+ *
178
+ * @minimum -90
179
+ * @maximum 90
180
+ */
181
+ public get latitude(): number {
182
+ return this.#latitude
183
+ }
184
+
185
+ public set latitude(value: number) {
186
+ this.#latitude = clampLatitude(value)
187
+ }
188
+
189
+ /**
190
+ * The altitude of the point, i.e. the z-coordinate.
191
+ *
192
+ * This is optional and is typically measured in meters.
193
+ */
194
+ public get altitude(): number {
195
+ return this.#altitude
196
+ }
197
+
198
+ public set altitude(value: number) {
199
+ this.#altitude = value
200
+ }
201
+
202
+ //#endregion
203
+
204
+ //#region Constructors
205
+
206
+ /**
207
+ * Create a new GeoPoint object with default coordinates.
208
+ */
209
+ constructor()
210
+ /**
211
+ * Create a new GeoPoint instance from another {@linkcode GeoJSONPosition} coordinates.
212
+ */
213
+ constructor(
214
+ geoJSONPosition: Point2DCoordinates | Point3DCoordinates,
215
+ bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox
216
+ )
217
+
218
+ /**
219
+ * Create a new GeoPoint instance from the browser's Geolocation API coordinates.
220
+ *
221
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates MDN Web Docs}
222
+ */
223
+ constructor(geoLocationCoordinates: GeolocationCoordinatesLike, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
224
+
225
+ /**
226
+ * Creates a new GeoPoint instance from a Google Maps API
227
+ * {@linkcode google.LatLngLiteral | LatLngLiteral} object.
228
+ */
229
+ constructor(latLngLiteral: google.LatLngLiteral, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
230
+
231
+ /**
232
+ * Creates a new GeoPoint instance from internal coordinates.
233
+ */
234
+ constructor(interpolatedCoords: InternalPointCoordinates, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
235
+ /**
236
+ * Creates a new GeoPoint instance from a Google Maps API {@linkcode google.LatLng | LatLng}
237
+ * object.
238
+ */
239
+ constructor(latLng: LatLngLiteral, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
240
+ /**
241
+ * Create a new GeoPoint instance from another {@linkcode PointLiteral}.
242
+ */
243
+ constructor(geoPointJSON: PointLiteral, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
244
+
245
+ /**
246
+ * Create a new GeoPoint instance.
247
+ */
248
+ constructor(input: GeoPointInput, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
249
+ constructor(input?: GeoPointInput, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox) {
250
+ if (isCoordPairLiteral(input)) {
251
+ if (input.length === 2) {
252
+ this.coordinates = inferGeoJSONCoordOrder(input)
253
+ } else {
254
+ this.coordinates = input
255
+ }
256
+ } else if (isPointLiteral(input)) {
257
+ this.coordinates = [...input.coordinates]
258
+
259
+ if (isBBox(input.bbox)) {
260
+ this.bbox = [...input.bbox]
261
+ }
262
+ } else if (isGoogleMapsLatLngLiteral(input)) {
263
+ this.coordinates = [input.lng, input.lat]
264
+ } else if (isGeolocationCoordinatesLike(input)) {
265
+ this.#longitude = input.longitude
266
+ this.#latitude = input.latitude
267
+ this.#altitude = input.altitude || 0
268
+ } else if (isInterpolatedCoordinates(input)) {
269
+ this.#longitude = input.x
270
+ this.#latitude = input.y
271
+ this.#altitude = 0
272
+ } else {
273
+ this.coordinates = [0, 0]
274
+ }
275
+
276
+ if (isBBox(bbox)) {
277
+ this.bbox = bbox
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Attempts to create a new GeoPoint instance from the given input.
283
+ */
284
+ static from(input: unknown): GeoPoint | null {
285
+ if (!input) return null
286
+ if (input instanceof GeoPoint) return input
287
+
288
+ if (typeof input === "string") {
289
+ const coordinates = tryParsingJSON<GeoPointInput>(input) || tryParsingJSON<GeoPointInput>(`[${input}]`)
290
+
291
+ if (coordinates) {
292
+ input = coordinates
293
+ }
294
+ }
295
+
296
+ try {
297
+ const point = new GeoPoint(input as GeoPointInput)
298
+ if (point.isNullIsland()) return null
299
+
300
+ return point
301
+ } catch (_error) {
302
+ return null
303
+ }
304
+ }
305
+
306
+ //#endregion
307
+
308
+ //#region Predicates
309
+
310
+ /**
311
+ * Whether the GeoPoint is 2-dimensional.
312
+ */
313
+ public is2D() {
314
+ return !this.is3D()
315
+ }
316
+
317
+ /**
318
+ * Whether the GeoPoint is 3-dimensional.
319
+ */
320
+ public is3D() {
321
+ return this.#altitude !== 0
322
+ }
323
+
324
+ /**
325
+ * Whether the GeoPoint is the null island at 0, 0.
326
+ */
327
+ public isNullIsland(): boolean {
328
+ return this.#latitude === 0 && this.#longitude === 0
329
+ }
330
+
331
+ //#endregion
332
+
333
+ //#region Conversion
334
+
335
+ public toJSON(): PointLiteral {
336
+ return {
337
+ type: this.type,
338
+ coordinates: this.coordinates,
339
+ }
340
+ }
341
+
342
+ public to2DCoordinates(): Point2DCoordinates {
343
+ return [this.#longitude, this.#latitude]
344
+ }
345
+
346
+ public to3DCoordinates(): Point3DCoordinates {
347
+ return [this.#longitude, this.#latitude, this.#altitude]
348
+ }
349
+
350
+ /**
351
+ * Converts the GeoPoint to a Google Maps API {@linkcode google.LatLngLiteral | LatLngLiteral}
352
+ * object.
353
+ */
354
+ public toGoogleLatLngLiteral(): google.LatLngLiteral {
355
+ return {
356
+ lat: this.#latitude,
357
+ lng: this.#longitude,
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Converts the GeoPoint to DMS (Degrees, Minutes, Seconds) format.
363
+ */
364
+ public toDMS(): string {
365
+ const converter = convertCoords(`${this.#latitude},${this.#longitude}`)
366
+
367
+ return converter.toCoordinateFormat("DMS")
368
+ }
369
+
370
+ /**
371
+ * Converts the GeoPoint to a H3 short cell address.
372
+ */
373
+ public toH3Cell(resolution = 15) {
374
+ const cell = latLngToCell(this.#latitude, this.#longitude, resolution) as H3Cell
375
+
376
+ return cell
377
+ }
378
+
379
+ /**
380
+ * Converts the GeoPoint to a H3 short cell address.
381
+ */
382
+ public toH3ShortCell(resolution = 15) {
383
+ const cell = this.toH3Cell(resolution)
384
+
385
+ return shortenH3Cell(cell)
386
+ }
387
+
388
+ public toString(): string {
389
+ return JSON.stringify(this.toJSON())
390
+ }
391
+ //#endregion
392
+
393
+ public [Symbol.iterator](): Iterator<number> {
394
+ return this.coordinates[Symbol.iterator]()
395
+ }
396
+ }
397
+
398
+ /**
399
+ * An array of positions for each point in the geometry.
400
+ *
401
+ * @see {@linkcode GeoJSONPosition} for more information.
402
+ */
403
+ export type MultiPointPath = [...points: Array<Point2DCoordinates | Point3DCoordinates>]
404
+
405
+ /**
406
+ * A collection of points, such as a constellation or a set of locations.
407
+ */
408
+ export interface MultiPointLiteral extends GeoObjectLiteral {
409
+ /**
410
+ * Declares the type of GeoJSON object as a `MultiPoint` geometry.
411
+ */
412
+ type: "MultiPoint"
413
+ /**
414
+ * An array of positions for each point in the geometry.
415
+ *
416
+ * @see {@linkcode GeoJSONPosition} for more information.
417
+ */
418
+ coordinates: MultiPointPath
419
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import { ResourceError } from "@mailwoman/core/errors"
8
+ import type { GeoObjectLiteral } from "../objects.js"
9
+ import type { LineStringPath } from "./line-string.js"
10
+
11
+ /**
12
+ * An array of positions forming a closed shape, such as a country or a lake.
13
+ *
14
+ * @example
15
+ *
16
+ * A polygon without holes:
17
+ *
18
+ * ```json
19
+ * {
20
+ * "type": "Polygon",
21
+ * "coordinates": [
22
+ * [
23
+ * [100, 0],
24
+ * [101, 0],
25
+ * [101, 1],
26
+ * [100, 1],
27
+ * [100, 0]
28
+ * ]
29
+ * ]
30
+ * }
31
+ * ```
32
+ */
33
+ export type SolidPolygonPath = [
34
+ /**
35
+ * - A linear ring is a closed LineString with four or more positions.
36
+ * - The first and last positions are equivalent (they represent equivalent points).
37
+ */
38
+ exteriorRing: LineStringPath,
39
+ ]
40
+
41
+ /**
42
+ * An array of positions forming a closed shape with holes, such as a country with islands or a lake
43
+ * with islands.
44
+ *
45
+ * @example
46
+ *
47
+ * A polygon with holes:
48
+ *
49
+ * ```json
50
+ * {
51
+ * "type": "Polygon",
52
+ * "coordinates": [
53
+ * [
54
+ * [100.0, 0.0],
55
+ * [101.0, 0.0],
56
+ * [101.0, 1.0],
57
+ * [100.0, 1.0],
58
+ * [100.0, 0.0]
59
+ * ],
60
+ * [
61
+ * [100.8, 0.8],
62
+ * [100.8, 0.2],
63
+ * [100.2, 0.2],
64
+ * [100.2, 0.8],
65
+ * [100.8, 0.8]
66
+ * ]
67
+ * ]
68
+ * }
69
+ * ```
70
+ */
71
+ export type NestedPolygonPath = [
72
+ /**
73
+ * - A linear ring is a closed LineString with four or more positions.
74
+ * - The first and last positions are equivalent (they represent equivalent points).
75
+ */
76
+ exteriorRing: LineStringPath,
77
+ /**
78
+ * - The interior rings are arrays of positions forming holes in the polygon.
79
+ */
80
+ ...interiorRings: LineStringPath[],
81
+ ]
82
+
83
+ /**
84
+ * A polygon geometry.
85
+ *
86
+ * @see {@linkcode PolygonLiteral} for applicable JSON schema.
87
+ * @see {@linkcode SolidPolygonPath} for more information.
88
+ * @see {@linkcode NestedPolygonPath} for more information.
89
+ */
90
+ export type PolygonPath = SolidPolygonPath | NestedPolygonPath
91
+
92
+ /**
93
+ * An array of positions forming a closed shape, such as a country or a lake.
94
+ */
95
+ export interface PolygonLiteral<P extends PolygonPath = SolidPolygonPath> extends GeoObjectLiteral {
96
+ /**
97
+ * Declares the type of GeoJSON object as a `Polygon` geometry.
98
+ */
99
+ type: "Polygon"
100
+ /**
101
+ * An array of positions for each point in the geometry.
102
+ *
103
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6 | RFC 7946 Section 3.1.6}
104
+ * @see {@linkcode SolidPolygonPath}
105
+ * @see {@linkcode NestedPolygonPath}
106
+ */
107
+ coordinates: P
108
+ }
109
+
110
+ /**
111
+ * Predicate for checking if a GeoJSON object is a `Polygon` geometry.
112
+ */
113
+ export function isPolygonLiteral<P extends PolygonPath = PolygonPath>(input: unknown): input is PolygonLiteral<P> {
114
+ if (typeof input !== "object" || input === null) return false
115
+
116
+ return "type" in input && input.type === "Polygon" && "coordinates" in input && Array.isArray(input.coordinates)
117
+ }
118
+
119
+ /**
120
+ * Predicate for checking if a polygon geometry is a solid, i.e. it has no holes.
121
+ */
122
+ export function isSolidPolygonPath(input: PolygonLiteral): boolean {
123
+ return input.coordinates.length === 1
124
+ }
125
+
126
+ /**
127
+ * A collection of polygons, such as a country with islands or a lake with islands.
128
+ */
129
+ export interface MultiPolygonLiteral<P extends PolygonPath = SolidPolygonPath> extends GeoObjectLiteral {
130
+ type: "MultiPolygon"
131
+
132
+ /**
133
+ * An array of polygons.
134
+ */
135
+ coordinates: P[][]
136
+ }
137
+ /**
138
+ * Predicate for checking if a GeoJSON object is a `MultiPolygon` geometry.
139
+ */
140
+
141
+ /**
142
+ * Given a polygon geometry, return an OSM filter string.
143
+ *
144
+ * This is useful when working with the Overpass API.
145
+ */
146
+ export function polygonToOSMFilter(input: PolygonLiteral): string {
147
+ if (!isPolygonLiteral(input)) return ""
148
+
149
+ const [exteriorRing] = input.coordinates
150
+
151
+ const filter = exteriorRing.map(([lon, lat]) => `${lat} ${lon}`).join(" ")
152
+
153
+ return `poly:'${filter}'`
154
+ }
155
+
156
+ /**
157
+ * Tags returned by the Overpass API for a node.
158
+ *
159
+ * @category OSM
160
+ */
161
+ export enum OSMNodeTag {
162
+ HouseNumber = "addr:housenumber",
163
+ PostCode = "addr:postcode",
164
+ Street = "addr:street",
165
+ State = "addr:state",
166
+ City = "addr:city",
167
+ Website = "website",
168
+ Email = "email",
169
+ Phone = "phone",
170
+ Shop = "shop",
171
+ Brand = "brand",
172
+ Cuisine = "cuisine",
173
+ Name = "name",
174
+ Healthcare = "healthcare",
175
+ Office = "office",
176
+ Amenity = "amenity",
177
+ }
178
+
179
+ export const ForbiddenResidentialOSMNodeTags: ReadonlySet<OSMNodeTag> = new Set<OSMNodeTag>([
180
+ OSMNodeTag.Shop,
181
+ OSMNodeTag.Brand,
182
+ OSMNodeTag.Cuisine,
183
+ OSMNodeTag.Office,
184
+ OSMNodeTag.Healthcare,
185
+ ])
186
+
187
+ export type OSMNodeTagRecord = Record<OSMNodeTag, string | undefined>
188
+
189
+ export interface OSMOverpassElement {
190
+ type: "node"
191
+ id: number
192
+ lat: number
193
+ lon: number
194
+ tags: OSMNodeTagRecord
195
+ }
196
+
197
+ export interface OSMOverpassResponseBody {
198
+ version: string
199
+ generator: string
200
+ osm3s: {
201
+ timestamp_osm_base: string
202
+ copyright: string
203
+ }
204
+ elements: OSMOverpassElement[]
205
+ }
206
+
207
+ /**
208
+ * Given an OSM element, attempts to infer if the result is a residential address.
209
+ */
210
+ export function isResidentialElement(element: OSMOverpassElement): boolean {
211
+ for (const key in element.tags) {
212
+ if (ForbiddenResidentialOSMNodeTags.has(key as OSMNodeTag)) return false
213
+ }
214
+
215
+ if (element.tags[OSMNodeTag.Amenity] === "restaurant") return false
216
+
217
+ return true
218
+ }
219
+
220
+ export function fetchOSMElementViaOverpassAPI(input: PolygonLiteral): Promise<OSMOverpassElement[]> {
221
+ const filter = polygonToOSMFilter(input)
222
+
223
+ const url = new URL("http://overpass-api.de/api/interpreter")
224
+ url.searchParams.set("data", `[out:json];(node['addr:housenumber'](${filter}););out body;>;out skel qt;`)
225
+
226
+ return fetch(url)
227
+ .then((response) => {
228
+ if (!response.ok) throw ResourceError.from(response.status, response.statusText, "osm", "overpass-api", "fetch")
229
+
230
+ return response
231
+ })
232
+ .then((response) => response.json() as Promise<OSMOverpassResponseBody>)
233
+ .then((body) => body.elements)
234
+ .catch((error) => {
235
+ throw ResourceError.wrap(error, "osm", "overpass-api", "fetch")
236
+ })
237
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ export * from "./place-id.js"
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Google Place ID utilities.
7
+ */
8
+
9
+ import type { Tagged } from "type-fest"
10
+
11
+ /**
12
+ * A Place ID uniquely identifies a place in the Google Places database and on Google Maps.
13
+ *
14
+ * The length of the identifier may vary. Generally, the identifier is a 27-character string,
15
+ * however, more specific places may have longer identifiers.
16
+ *
17
+ * Place IDs appear to be base64-encoded strings, delimited by underscores and dashes.
18
+ *
19
+ * Note that Place IDs do change. Consider a them stale after a few days.
20
+ *
21
+ * @category Google
22
+ * @category Geocoding
23
+ * @type {string}
24
+ * @minLength 1
25
+ * @pattern ^[A-Za-z0-9_-]+$
26
+ * @title Google Place ID
27
+ */
28
+ export type GooglePlaceID = Tagged<string, "GooglePlaceID">
29
+
30
+ /**
31
+ * Pattern for validating a Google Place ID.
32
+ */
33
+ export const GOOGLE_PLACE_ID_PATTERN = /^[A-Za-z0-9_-]+$/
34
+
35
+ /**
36
+ * Type-predicate for checking if a value appears to be a valid Google Place ID.
37
+ *
38
+ * @category Google
39
+ * @category Geocoding
40
+ * @internal
41
+ */
42
+ export function isGooglePlaceID(input: string): input is GooglePlaceID {
43
+ return GOOGLE_PLACE_ID_PATTERN.test(input.toString())
44
+ }