web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit db239fc4efac2d1911de429466fe3442cf66b737
parent bad0798d404bba40617a5407a36c799b4dd711d7
Author: triesap <triesap@radroots.dev>
Date:   Sat, 27 Dec 2025 18:40:46 +0000

geocoder: support configurable database and connect options

- Add GeocoderConfig/GeocoderConnectConfig and update IGeocoder.connect signature
- Normalize connect config and allow string shorthand for wasm_path
- Resolve database path via route-aware helper and default geonames location
- Export database path utilities/constants from geocoder entrypoints

Diffstat:
Mgeocoder/src/geocoder.ts | 29+++++++++++++++++++----------
Mgeocoder/src/index.ts | 5++---
Mgeocoder/src/types.ts | 10+++++++++-
Mgeocoder/src/utils.ts | 35+++++++++++++++++++++++++++++++++++
Mutils/src/path/index.ts | 50+++++++++++++++++++++++++++++++++++++++++++++-----
5 files changed, 110 insertions(+), 19 deletions(-)

diff --git a/geocoder/src/geocoder.ts b/geocoder/src/geocoder.ts @@ -1,29 +1,38 @@ import type { GeolocationPoint } from "@radroots/geo"; import { err_msg, resolve_wasm_path } from "@radroots/utils"; import type { Database } from "sql.js"; -import type { GeocoderReverseResult, IGeocoder, IGeocoderConnectResolve, IGeocoderCountryCenter, IGeocoderCountryCenterResolve, IGeocoderCountryListResolve, IGeocoderCountryListResult, IGeocoderCountryResolve, IGeocoderReverseOpts, IGeocoderReverseResolve } from "./types.js"; -import { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result } from "./utils.js"; +import type { GeocoderConfig, GeocoderConnectConfig, GeocoderReverseResult, IGeocoder, IGeocoderConnectResolve, IGeocoderCountryCenter, IGeocoderCountryCenterResolve, IGeocoderCountryListResolve, IGeocoderCountryListResult, IGeocoderCountryResolve, IGeocoderReverseOpts, IGeocoderReverseResolve } from "./types.js"; +import { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result, resolve_geocoder_database_path } from "./utils.js"; const KM_PER_DEGREE_LATITUDE = 111; const DEFAULT_SQL_WASM_PATH = `/assets/sql-wasm.wasm`; -const DEFAULT_SQL_DATABASE_PATH = `/assets/geonames.db`; + +const normalize_geocoder_connect_config = (config?: GeocoderConnectConfig | string): GeocoderConnectConfig => { + if (!config) return {}; + if (typeof config === `string`) return { wasm_path: config }; + return config; +}; export class Geocoder implements IGeocoder { private _db: Database | null = null; - private _database_name: string; + private _database_path: string; - constructor(database_name?: string) { - if (database_name && database_name.charAt(0) !== `/`) throw new Error(`Error: database name must be a valid path`); - this._database_name = database_name || DEFAULT_SQL_DATABASE_PATH; + constructor(config?: GeocoderConfig | string) { + const database_path = typeof config === `string` ? config : config?.database_path; + this._database_path = resolve_geocoder_database_path(database_path); } - public async connect(wasm_path?: string): Promise<IGeocoderConnectResolve> { + public async connect(config?: GeocoderConnectConfig | string): Promise<IGeocoderConnectResolve> { try { + const connect_config = normalize_geocoder_connect_config(config); + const database_path = connect_config.database_path + ? resolve_geocoder_database_path(connect_config.database_path) + : this._database_path; const init_sqljs = await import(`sql.js`); const sql = await init_sqljs.default({ - locateFile: wasm_file => resolve_wasm_path(wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH) + locateFile: wasm_file => resolve_wasm_path(connect_config.wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH) }); - const database_res = await fetch(this._database_name); + const database_res = await fetch(database_path); const database_buffer = await database_res.arrayBuffer(); this._db = new sql.Database(new Uint8Array(database_buffer)); return true; diff --git a/geocoder/src/index.ts b/geocoder/src/index.ts @@ -1,4 +1,3 @@ export { Geocoder } from "./geocoder.js" -export type { GeocoderDegreeOffset, GeocoderIError, GeocoderReverseResult, IGeocoder, IGeocoderCountryCenter, IGeocoderCountryListResult, IGeocoderReverseOpts } from "./types.js" -export { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result } from "./utils.js" - +export type { GeocoderConfig, GeocoderConnectConfig, GeocoderDegreeOffset, GeocoderIError, GeocoderReverseResult, IGeocoder, IGeocoderCountryCenter, IGeocoderCountryListResult, IGeocoderReverseOpts } from "./types.js" +export { DEFAULT_GEOCODER_DATABASE_FILE, DEFAULT_GEOCODER_DATABASE_PATH, parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result, parse_geocoder_database_route, resolve_geocoder_database_path, type GeocoderDatabaseRoute } from "./utils.js" diff --git a/geocoder/src/types.ts b/geocoder/src/types.ts @@ -21,6 +21,14 @@ export type GeocoderReverseResult = { export type GeocoderDegreeOffset = 0.5 | 1.0 | 1.5 | 2.0 | 2.5 | 3 +export type GeocoderConfig = { + database_path?: string; +}; + +export type GeocoderConnectConfig = GeocoderConfig & { + wasm_path?: string; +}; + export type IGeocoderReverseOpts = { degree_offset?: GeocoderDegreeOffset; limit?: number | false; @@ -39,7 +47,7 @@ export type IGeocoderCountryListResolve = ResultsList<IGeocoderCountryListResult export type IGeocoderCountryCenterResolve = ResultObj<GeolocationPoint> | IError<GeocoderIError>; export type IGeocoder = { - connect(wasm_path?: string): Promise<IGeocoderConnectResolve>; + connect(config?: GeocoderConnectConfig | string): Promise<IGeocoderConnectResolve>; reverse(point: GeolocationPoint, opts?: IGeocoderReverseOpts): Promise<IGeocoderReverseResolve>; country(opts: IGeocoderCountryCenter): Promise<IGeocoderCountryResolve>; country_list(): Promise<IGeocoderCountryListResolve>; diff --git a/geocoder/src/utils.ts b/geocoder/src/utils.ts @@ -1,6 +1,41 @@ import type { GeolocationPoint } from "@radroots/geo"; +import { parse_route_path, resolve_route_path } from "@radroots/utils"; import type { GeocoderReverseResult, IGeocoderCountryListResult } from "./types.js"; +export const DEFAULT_GEOCODER_DATABASE_FILE = `geonames.db`; +export const DEFAULT_GEOCODER_DATABASE_PATH = `/geonames/geonames.db`; +const GEOCODER_DATABASE_FILE_EXTS = [`.db`]; + +export type GeocoderDatabaseRoute = { + base_path: string; + file_name: string | null; + query: string; + hash: string; +}; + +const is_geocoder_database_file = (segment: string): boolean => { + const lower_segment = segment.toLowerCase(); + return GEOCODER_DATABASE_FILE_EXTS.some((ext) => lower_segment.endsWith(ext)); +}; + +export const parse_geocoder_database_route = (database_path: string): GeocoderDatabaseRoute => { + const { path, query, hash } = parse_route_path(database_path); + const trimmed_path = path.endsWith(`/`) ? path.slice(0, -1) : path; + const last_slash = trimmed_path.lastIndexOf(`/`); + const last_segment = last_slash >= 0 ? trimmed_path.slice(last_slash + 1) : trimmed_path; + const file_name = is_geocoder_database_file(last_segment) ? last_segment : null; + const base_path = file_name && last_slash >= 0 ? trimmed_path.slice(0, last_slash) : trimmed_path; + return { base_path, file_name, query, hash }; +}; + +export const resolve_geocoder_database_path = (database_path?: string): string => + resolve_route_path( + database_path, + DEFAULT_GEOCODER_DATABASE_FILE, + DEFAULT_GEOCODER_DATABASE_PATH, + GEOCODER_DATABASE_FILE_EXTS + ); + export const parse_geocode_reverse_result = (obj: any): GeocoderReverseResult | undefined => { if (typeof obj !== `object` || !obj) return undefined; const { id, name, admin1_id, admin1_name, country_id, country_name, latitude, longitude } = obj; diff --git a/utils/src/path/index.ts b/utils/src/path/index.ts @@ -1,6 +1,46 @@ -export const resolve_wasm_path = (wasm_path: string | undefined, wasm_file: string, default_wasm_path: string): string => { - const resolved_wasm_path = wasm_path ?? default_wasm_path; - if (resolved_wasm_path.endsWith(".wasm")) return resolved_wasm_path; - const base_path = resolved_wasm_path.endsWith("/") ? resolved_wasm_path.slice(0, -1) : resolved_wasm_path; - return `${base_path}/${wasm_file}`; +export type RoutePathParts = { + path: string; + query: string; + hash: string; }; + +export const parse_route_path = (route_path: string): RoutePathParts => { + const query_idx = route_path.indexOf("?"); + const hash_idx = route_path.indexOf("#"); + let path_end = route_path.length; + if (query_idx >= 0 && hash_idx >= 0) path_end = Math.min(query_idx, hash_idx); + else if (query_idx >= 0) path_end = query_idx; + else if (hash_idx >= 0) path_end = hash_idx; + const path = route_path.slice(0, path_end); + const query = query_idx >= 0 + ? route_path.slice(query_idx, hash_idx >= 0 ? hash_idx : undefined) + : ""; + const hash = hash_idx >= 0 ? route_path.slice(hash_idx) : ""; + return { path, query, hash }; +}; + +const has_file_extension = (route_path: string, file_exts: readonly string[]): boolean => { + const lower_path = route_path.toLowerCase(); + return file_exts.some((ext) => lower_path.endsWith(ext)); +}; + +export const resolve_route_path = ( + route_path: string | undefined, + file_name: string, + default_route_path: string, + file_exts: readonly string[] +): string => { + const resolved_route_path = route_path ?? default_route_path; + const { path, query, hash } = parse_route_path(resolved_route_path); + const normalized_path = path.endsWith("/") ? path.slice(0, -1) : path; + if (!normalized_path) return resolved_route_path; + if ( + normalized_path === file_name + || normalized_path.endsWith(`/${file_name}`) + || has_file_extension(normalized_path, file_exts) + ) return `${normalized_path}${query}${hash}`; + return `${normalized_path}/${file_name}${query}${hash}`; +}; + +export const resolve_wasm_path = (wasm_path: string | undefined, wasm_file: string, default_wasm_path: string): string => + resolve_route_path(wasm_path, wasm_file, default_wasm_path, [".wasm"]);