@tmlmobilidade/go-interfaces-locationsdb 20261001.1824.22

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,2 @@
1
+ export * from './interface.js';
2
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './interface.js';
2
+ export * from './types.js';
@@ -0,0 +1,40 @@
1
+ import { type QueryResult, type QueryResultRow } from '@tmlmobilidade/go-clients-postgres';
2
+ import { CountryCode, type Location, type LocationWithGeojson } from './types.js';
3
+ declare class LocationsDbClass {
4
+ private static _instance;
5
+ private readonly postgresClient;
6
+ private constructor();
7
+ /**
8
+ * Establishes a connection to the PostGIS / PostgreSQL database.
9
+ * @throws Error if required LOCATIONSDB_* environment variables are missing or if the connection fails.
10
+ */
11
+ static getInstance(): Promise<LocationsDbClass>;
12
+ /**
13
+ * Finds the administrative locations whose geometry covers a WGS84 point (typically country,
14
+ * district, municipality, parish), plus the nearest `place=locality` point within the given
15
+ * radius, returned with `admin_level = "neighbourhood"`.
16
+ * @param localityMaxDistanceMeters Search radius for the locality point.
17
+ */
18
+ findLocationsAtPoint(longitude: number, latitude: number, localityMaxDistanceMeters: number): Promise<Location[]>;
19
+ /**
20
+ * Lists locations for a given OSM admin_level inside a country, sorted by name, without geometry.
21
+ * @param countryCode ISO 3166-1 alpha-2 country code.
22
+ * @param adminLevel OSM admin_level (e.g. `"7"` for Portuguese municipalities).
23
+ */
24
+ findLocationsByCountryAndAdminLevel(countryCode: CountryCode, adminLevel: number | string): Promise<Location[]>;
25
+ /**
26
+ * Lists locations for a given OSM admin_level inside a country, including GeoJSON geometry.
27
+ * @param countryCode ISO 3166-1 alpha-2 country code.
28
+ * @param adminLevel OSM admin_level (e.g. `"7"` for Portuguese municipalities).
29
+ */
30
+ findLocationsWithGeojsonByCountryAndAdminLevel(countryCode: CountryCode, adminLevel: number | string): Promise<LocationWithGeojson[]>;
31
+ /**
32
+ * Executes a SQL query against the locations database.
33
+ * @param text The SQL query text (supports `$1`, `$2`, … placeholders).
34
+ * @param params Optional positional query parameters.
35
+ * @returns The query result rows typed as `T`.
36
+ */
37
+ query<T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]): Promise<QueryResult<T>>;
38
+ }
39
+ export declare const locationsDb: LocationsDbClass;
40
+ export {};
@@ -0,0 +1,63 @@
1
+ /* * */
2
+ import { PostgresDatabaseClient } from '@tmlmobilidade/go-clients-postgres';
3
+ import { asyncSingletonProxy } from '@tmlmobilidade/go-utils-exec';
4
+ import { FIND_LOCATIONS_AT_POINT, FIND_LOCATIONS_BY_COUNTRY_AND_ADMIN_LEVEL, FIND_LOCATIONS_WITH_GEOJSON_BY_COUNTRY_AND_ADMIN_LEVEL } from './queries.js';
5
+ /* * */
6
+ class LocationsDbClass {
7
+ //
8
+ static _instance;
9
+ postgresClient;
10
+ constructor(postgresClient) {
11
+ this.postgresClient = postgresClient;
12
+ }
13
+ /**
14
+ * Establishes a connection to the PostGIS / PostgreSQL database.
15
+ * @throws Error if required LOCATIONSDB_* environment variables are missing or if the connection fails.
16
+ */
17
+ static async getInstance() {
18
+ if (!LocationsDbClass._instance) {
19
+ const postgresClient = await PostgresDatabaseClient.getClient({ prefix: 'LOCATIONSDB', tunnelType: 'GO' });
20
+ LocationsDbClass._instance = new LocationsDbClass(postgresClient);
21
+ }
22
+ return LocationsDbClass._instance;
23
+ }
24
+ /**
25
+ * Finds the administrative locations whose geometry covers a WGS84 point (typically country,
26
+ * district, municipality, parish), plus the nearest `place=locality` point within the given
27
+ * radius, returned with `admin_level = "neighbourhood"`.
28
+ * @param localityMaxDistanceMeters Search radius for the locality point.
29
+ */
30
+ async findLocationsAtPoint(longitude, latitude, localityMaxDistanceMeters) {
31
+ const result = await this.postgresClient.query(FIND_LOCATIONS_AT_POINT, [longitude, latitude, localityMaxDistanceMeters]);
32
+ return result.rows;
33
+ }
34
+ /**
35
+ * Lists locations for a given OSM admin_level inside a country, sorted by name, without geometry.
36
+ * @param countryCode ISO 3166-1 alpha-2 country code.
37
+ * @param adminLevel OSM admin_level (e.g. `"7"` for Portuguese municipalities).
38
+ */
39
+ async findLocationsByCountryAndAdminLevel(countryCode, adminLevel) {
40
+ const result = await this.postgresClient.query(FIND_LOCATIONS_BY_COUNTRY_AND_ADMIN_LEVEL, [String(adminLevel), countryCode]);
41
+ return result.rows;
42
+ }
43
+ /**
44
+ * Lists locations for a given OSM admin_level inside a country, including GeoJSON geometry.
45
+ * @param countryCode ISO 3166-1 alpha-2 country code.
46
+ * @param adminLevel OSM admin_level (e.g. `"7"` for Portuguese municipalities).
47
+ */
48
+ async findLocationsWithGeojsonByCountryAndAdminLevel(countryCode, adminLevel) {
49
+ const result = await this.postgresClient.query(FIND_LOCATIONS_WITH_GEOJSON_BY_COUNTRY_AND_ADMIN_LEVEL, [String(adminLevel), countryCode]);
50
+ return result.rows;
51
+ }
52
+ /**
53
+ * Executes a SQL query against the locations database.
54
+ * @param text The SQL query text (supports `$1`, `$2`, … placeholders).
55
+ * @param params Optional positional query parameters.
56
+ * @returns The query result rows typed as `T`.
57
+ */
58
+ async query(text, params) {
59
+ return await this.postgresClient.query(text, params);
60
+ }
61
+ }
62
+ /* * */
63
+ export const locationsDb = asyncSingletonProxy(LocationsDbClass);
@@ -0,0 +1,13 @@
1
+ /** List locations at a given admin_level ($1) inside a country ($2), without geometry. */
2
+ export declare const FIND_LOCATIONS_BY_COUNTRY_AND_ADMIN_LEVEL = "\nSELECT\n\tabs(p.osm_id) AS id,\n\tCOALESCE(p.tags->>'int_name', p.name) AS name,\n\tp.admin_level,\n\tp.tags->>'ref:ine' AS code,\n\tp.tags\nFROM planet_osm_polygon p\nWHERE p.boundary = 'administrative'\n\tAND p.admin_level = $1\n\tAND p.way && (\n\tSELECT c.way\n\tFROM planet_osm_polygon c\n\tWHERE c.boundary = 'administrative'\n\t\tAND c.admin_level = '2'\n\t\tAND c.tags->>'ISO3166-1' = $2\n\tLIMIT 1\n)\n\tAND ST_Within(p.way, (\n\tSELECT c.way\n\tFROM planet_osm_polygon c\n\tWHERE c.boundary = 'administrative'\n\t\tAND c.admin_level = '2'\n\t\tAND c.tags->>'ISO3166-1' = $2\n\tLIMIT 1\n))\nORDER BY name;\n";
3
+ /** List locations at a given admin_level ($1) inside a country ($2), with GeoJSON geometry. */
4
+ export declare const FIND_LOCATIONS_WITH_GEOJSON_BY_COUNTRY_AND_ADMIN_LEVEL = "\nSELECT\n\tabs(p.osm_id) AS id,\n\tCOALESCE(p.tags->>'int_name', p.name) AS name,\n\tp.admin_level,\n\tp.tags->>'ref:ine' AS code,\n\tp.tags,\n\tjson_build_object(\n\t\t'type', 'FeatureCollection',\n\t\t'features', json_agg(\n\t\t\tjson_build_object(\n\t\t\t\t'type', 'Feature',\n\t\t\t\t'id', abs(osm_id),\n\t\t\t\t'properties', json_build_object(\n\t\t\t\t\t'name', name,\n\t\t\t\t\t'admin_level', admin_level,\n\t\t\t\t\t'code', tags->>'ref:ine',\n\t\t\t\t\t'tags', tags\n\t\t\t\t),\n\t\t\t\t'geometry', ST_AsGeoJSON(\n\t\t\t\t\tST_Transform(way, 4326)\n\t\t\t\t)::json\n\t\t\t)\n\t\t)\n\t) AS geojson\nFROM planet_osm_polygon p\nWHERE p.boundary = 'administrative'\n\tAND p.admin_level = $1\n\tAND p.way && (\n\tSELECT c.way\n\tFROM planet_osm_polygon c\n\tWHERE c.boundary = 'administrative'\n\t\tAND c.admin_level = '2'\n\t\tAND c.tags->>'ISO3166-1' = $2\n\tLIMIT 1\n)\n\tAND ST_Within(p.way, (\n\tSELECT c.way\n\tFROM planet_osm_polygon c\n\tWHERE c.boundary = 'administrative'\n\t\tAND c.admin_level = '2'\n\t\tAND c.tags->>'ISO3166-1' = $2\n\tLIMIT 1\n))\nGROUP BY id, name, admin_level, code, tags;\n";
5
+ /**
6
+ * Find the administrative locations that cover a WGS84 point ($1 lon, $2 lat), plus the nearest
7
+ * `place=locality` point within $3 metres (returned with `admin_level = 'neighbourhood'`), in one round-trip.
8
+ * `way` is WGS84 geometry (SRID 4326). Prefilter neighbourhoods with the geometry index
9
+ * before applying the exact geography distance. The conservative degree radius uses less
10
+ * than the minimum metres per latitude degree and the most extreme latitude in the cap.
11
+ * Caps crossing the antimeridian or a pole use worldwide bounds to avoid dropping matches.
12
+ */
13
+ export declare const FIND_LOCATIONS_AT_POINT = "\nWITH point AS (\n\tSELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS way,\n\t\t$3::double precision / (\n\t\t\t100000 * GREATEST(cos(radians(LEAST(90, abs($2) + $3::double precision / 100000))), 0.000001)\n\t\t) AS degree_radius\n), pt AS (\n\tSELECT way,\n\t\tCASE WHEN abs($1) + degree_radius >= 180 OR abs($2) + degree_radius >= 90\n\t\t\tTHEN ST_MakeEnvelope(-180, -90, 180, 90, 4326)\n\t\t\tELSE ST_Expand(way, degree_radius)\n\t\tEND AS bounds\n\tFROM point\n)\nSELECT\n\tabs(p.osm_id) AS id,\n\tCOALESCE(p.tags->>'int_name', p.name) AS name,\n\tp.admin_level,\n\tp.tags->>'ref:ine' AS code,\n\tp.tags\nFROM planet_osm_polygon p, pt\nWHERE p.boundary = 'administrative'\n\tAND ST_Covers(p.way, pt.way)\nUNION ALL\n(\n\tSELECT\n\t\tabs(p.osm_id),\n\t\tCOALESCE(p.tags->>'int_name', p.name),\n\t\t'neighbourhood',\n\t\tNULL,\n\t\tp.tags\n\tFROM planet_osm_point p, pt\n\tWHERE p.place = 'neighbourhood'\n\t\tAND p.name IS NOT NULL\n\t\tAND p.way && pt.bounds\n\t\tAND ST_DWithin(p.way::geography, pt.way::geography, $3)\n\tORDER BY p.way::geography <-> pt.way::geography\n\tLIMIT 1\n);\n";
@@ -0,0 +1,106 @@
1
+ /* * */
2
+ /** The admin_level 2 polygon of a country, by ISO 3166-1 alpha-2 code ($2). */
3
+ const COUNTRY_WAY = `
4
+ SELECT c.way
5
+ FROM planet_osm_polygon c
6
+ WHERE c.boundary = 'administrative'
7
+ AND c.admin_level = '2'
8
+ AND c.tags->>'ISO3166-1' = $2
9
+ LIMIT 1
10
+ `;
11
+ /** List locations at a given admin_level ($1) inside a country ($2), without geometry. */
12
+ export const FIND_LOCATIONS_BY_COUNTRY_AND_ADMIN_LEVEL = `
13
+ SELECT
14
+ abs(p.osm_id) AS id,
15
+ COALESCE(p.tags->>'int_name', p.name) AS name,
16
+ p.admin_level,
17
+ p.tags->>'ref:ine' AS code,
18
+ p.tags
19
+ FROM planet_osm_polygon p
20
+ WHERE p.boundary = 'administrative'
21
+ AND p.admin_level = $1
22
+ AND p.way && (${COUNTRY_WAY})
23
+ AND ST_Within(p.way, (${COUNTRY_WAY}))
24
+ ORDER BY name;
25
+ `;
26
+ /** List locations at a given admin_level ($1) inside a country ($2), with GeoJSON geometry. */
27
+ export const FIND_LOCATIONS_WITH_GEOJSON_BY_COUNTRY_AND_ADMIN_LEVEL = `
28
+ SELECT
29
+ abs(p.osm_id) AS id,
30
+ COALESCE(p.tags->>'int_name', p.name) AS name,
31
+ p.admin_level,
32
+ p.tags->>'ref:ine' AS code,
33
+ p.tags,
34
+ json_build_object(
35
+ 'type', 'FeatureCollection',
36
+ 'features', json_agg(
37
+ json_build_object(
38
+ 'type', 'Feature',
39
+ 'id', abs(osm_id),
40
+ 'properties', json_build_object(
41
+ 'name', name,
42
+ 'admin_level', admin_level,
43
+ 'code', tags->>'ref:ine',
44
+ 'tags', tags
45
+ ),
46
+ 'geometry', ST_AsGeoJSON(
47
+ ST_Transform(way, 4326)
48
+ )::json
49
+ )
50
+ )
51
+ ) AS geojson
52
+ FROM planet_osm_polygon p
53
+ WHERE p.boundary = 'administrative'
54
+ AND p.admin_level = $1
55
+ AND p.way && (${COUNTRY_WAY})
56
+ AND ST_Within(p.way, (${COUNTRY_WAY}))
57
+ GROUP BY id, name, admin_level, code, tags;
58
+ `;
59
+ /**
60
+ * Find the administrative locations that cover a WGS84 point ($1 lon, $2 lat), plus the nearest
61
+ * `place=locality` point within $3 metres (returned with `admin_level = 'neighbourhood'`), in one round-trip.
62
+ * `way` is WGS84 geometry (SRID 4326). Prefilter neighbourhoods with the geometry index
63
+ * before applying the exact geography distance. The conservative degree radius uses less
64
+ * than the minimum metres per latitude degree and the most extreme latitude in the cap.
65
+ * Caps crossing the antimeridian or a pole use worldwide bounds to avoid dropping matches.
66
+ */
67
+ export const FIND_LOCATIONS_AT_POINT = `
68
+ WITH point AS (
69
+ SELECT ST_SetSRID(ST_MakePoint($1, $2), 4326) AS way,
70
+ $3::double precision / (
71
+ 100000 * GREATEST(cos(radians(LEAST(90, abs($2) + $3::double precision / 100000))), 0.000001)
72
+ ) AS degree_radius
73
+ ), pt AS (
74
+ SELECT way,
75
+ CASE WHEN abs($1) + degree_radius >= 180 OR abs($2) + degree_radius >= 90
76
+ THEN ST_MakeEnvelope(-180, -90, 180, 90, 4326)
77
+ ELSE ST_Expand(way, degree_radius)
78
+ END AS bounds
79
+ FROM point
80
+ )
81
+ SELECT
82
+ abs(p.osm_id) AS id,
83
+ COALESCE(p.tags->>'int_name', p.name) AS name,
84
+ p.admin_level,
85
+ p.tags->>'ref:ine' AS code,
86
+ p.tags
87
+ FROM planet_osm_polygon p, pt
88
+ WHERE p.boundary = 'administrative'
89
+ AND ST_Covers(p.way, pt.way)
90
+ UNION ALL
91
+ (
92
+ SELECT
93
+ abs(p.osm_id),
94
+ COALESCE(p.tags->>'int_name', p.name),
95
+ 'neighbourhood',
96
+ NULL,
97
+ p.tags
98
+ FROM planet_osm_point p, pt
99
+ WHERE p.place = 'neighbourhood'
100
+ AND p.name IS NOT NULL
101
+ AND p.way && pt.bounds
102
+ AND ST_DWithin(p.way::geography, pt.way::geography, $3)
103
+ ORDER BY p.way::geography <-> pt.way::geography
104
+ LIMIT 1
105
+ );
106
+ `;
@@ -0,0 +1,20 @@
1
+ import { type FeatureCollection, type Geometry } from 'geojson';
2
+ /** OSM tag bag stored as jsonb on planet_osm_polygon. */
3
+ export type OsmTags = Record<string, string>;
4
+ /** Properties embedded in each GeoJSON Feature of a location. */
5
+ export interface LocationProperties {
6
+ admin_level: null | string;
7
+ code: null | string;
8
+ name: null | string;
9
+ tags: OsmTags;
10
+ }
11
+ /** Location row from planet_osm_polygon. */
12
+ export interface Location extends LocationProperties {
13
+ id: string;
14
+ }
15
+ /** Location row including a GeoJSON FeatureCollection of its geometry. */
16
+ export interface LocationWithGeojson extends Location {
17
+ geojson: FeatureCollection<Geometry, LocationProperties>;
18
+ }
19
+ /** ISO 3166-1 alpha-2 country code. */
20
+ export type CountryCode = 'AD' | 'AE' | 'AF' | 'AG' | 'AI' | 'AL' | 'AM' | 'AN' | 'AO' | 'AQ' | 'AR' | 'AS' | 'AT' | 'AU' | 'AW' | 'AX' | 'AZ' | 'BA' | 'BB' | 'BD' | 'BE' | 'BF' | 'BG' | 'BH' | 'BI' | 'BJ' | 'BL' | 'BM' | 'BN' | 'BN' | 'BO' | 'BO' | 'BQ' | 'BR' | 'BS' | 'BT' | 'BV' | 'BW' | 'BY' | 'BZ' | 'CA' | 'CC' | 'CD' | 'CF' | 'CG' | 'CH' | 'CI' | 'CI' | 'CK' | 'CL' | 'CM' | 'CN' | 'CO' | 'CR' | 'CU' | 'CV' | 'CW' | 'CX' | 'CY' | 'CZ' | 'DE' | 'DJ' | 'DK' | 'DM' | 'DO' | 'DZ' | 'EC' | 'EE' | 'EG' | 'EH' | 'ER' | 'ES' | 'ET' | 'FI' | 'FJ' | 'FK' | 'FM' | 'FO' | 'FR' | 'GA' | 'GB' | 'GD' | 'GE' | 'GF' | 'GG' | 'GH' | 'GI' | 'GL' | 'GM' | 'GN' | 'GP' | 'GQ' | 'GR' | 'GS' | 'GT' | 'GU' | 'GW' | 'GY' | 'HK' | 'HM' | 'HN' | 'HR' | 'HT' | 'HU' | 'ID' | 'IE' | 'IL' | 'IM' | 'IN' | 'IO' | 'IQ' | 'IR' | 'IS' | 'IT' | 'JE' | 'JM' | 'JO' | 'JP' | 'KE' | 'KG' | 'KH' | 'KI' | 'KM' | 'KN' | 'KP' | 'KR' | 'KR' | 'KW' | 'KY' | 'KZ' | 'LA' | 'LB' | 'LC' | 'LI' | 'LK' | 'LR' | 'LS' | 'LT' | 'LU' | 'LV' | 'LY' | 'LY' | 'MA' | 'MC' | 'MD' | 'ME' | 'MF' | 'MG' | 'MH' | 'MK' | 'ML' | 'MM' | 'MM' | 'MN' | 'MO' | 'MP' | 'MQ' | 'MR' | 'MS' | 'MT' | 'MU' | 'MV' | 'MW' | 'MX' | 'MY' | 'MZ' | 'NA' | 'NC' | 'NE' | 'NF' | 'NG' | 'NI' | 'NL' | 'NO' | 'NP' | 'NR' | 'NU' | 'NZ' | 'OM' | 'PA' | 'PE' | 'PF' | 'PG' | 'PH' | 'PK' | 'PL' | 'PM' | 'PN' | 'PR' | 'PS' | 'PT' | 'PW' | 'PY' | 'QA' | 'RE' | 'RO' | 'RS' | 'RU' | 'RU' | 'RW' | 'SA' | 'SB' | 'SC' | 'SD' | 'SE' | 'SG' | 'SH' | 'SI' | 'SJ' | 'SK' | 'SL' | 'SM' | 'SN' | 'SO' | 'SR' | 'SS' | 'ST' | 'SV' | 'SX' | 'SY' | 'SZ' | 'TC' | 'TD' | 'TF' | 'TG' | 'TH' | 'TJ' | 'TK' | 'TL' | 'TM' | 'TN' | 'TO' | 'TR' | 'TT' | 'TV' | 'TW' | 'TZ' | 'UA' | 'UG' | 'UM' | 'US' | 'UY' | 'UZ' | 'VA' | 'VC' | 'VC' | 'VC' | 'VE' | 'VE' | 'VG' | 'VI' | 'VN' | 'VN' | 'VU' | 'WF' | 'WS' | 'XK' | 'YE' | 'YT' | 'ZA' | 'ZM' | 'ZW';
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ /* * */
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@tmlmobilidade/go-interfaces-locationsdb",
3
+ "version": "20261001.1824.22",
4
+ "author": {
5
+ "email": "iso@tmlmobilidade.pt",
6
+ "name": "TML-ISO"
7
+ },
8
+ "license": "AGPL-3.0-or-later",
9
+ "homepage": "https://go.tmlmobilidade.pt",
10
+ "bugs": {
11
+ "url": "https://github.com/tmlmobilidade/go/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/tmlmobilidade/go.git"
16
+ },
17
+ "keywords": [
18
+ "public transit",
19
+ "tml",
20
+ "transportes metropolitanos de lisboa",
21
+ "go"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "scripts": {
33
+ "build": "tsc && resolve-tspaths",
34
+ "lint": "eslint ./src/ && tsc --noEmit",
35
+ "lint:fix": "eslint ./src/ --fix",
36
+ "watch": "tsc-watch --onSuccess 'resolve-tspaths'"
37
+ },
38
+ "dependencies": {
39
+ "@tmlmobilidade/go-clients-postgres": "*",
40
+ "@tmlmobilidade/go-utils-exec": "*"
41
+ },
42
+ "devDependencies": {
43
+ "@tmlmobilidade/go-utils-tsconfig": "*",
44
+ "@types/geojson": "7946.0.16",
45
+ "@types/node": "26.4.0",
46
+ "resolve-tspaths": "0.8.23",
47
+ "tsc-watch": "7.2.1",
48
+ "typescript": "6.0.3"
49
+ }
50
+ }