@hebcal/geo-sqlite 5.4.1 → 5.5.1

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.
Files changed (3) hide show
  1. package/dist/index.js +336 -4
  2. package/package.json +2 -1
  3. package/readme.txt +0 -141
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @hebcal/geo-sqlite v5.4.1 */
1
+ /*! @hebcal/geo-sqlite v5.5.1 */
2
2
  import Database from 'better-sqlite3';
3
3
  import { Location, Locale } from '@hebcal/core';
4
4
  import '@hebcal/cities';
@@ -6,6 +6,336 @@ import events from 'events';
6
6
  import fs from 'fs';
7
7
  import readline from 'readline';
8
8
 
9
+ class QuickLRU extends Map {
10
+ #size = 0;
11
+ #cache = new Map();
12
+ #oldCache = new Map();
13
+ #maxSize;
14
+ #maxAge;
15
+ #onEviction;
16
+
17
+ constructor(options = {}) {
18
+ super();
19
+
20
+ if (!(options.maxSize && options.maxSize > 0)) {
21
+ throw new TypeError('`maxSize` must be a number greater than 0');
22
+ }
23
+
24
+ if (typeof options.maxAge === 'number' && options.maxAge === 0) {
25
+ throw new TypeError('`maxAge` must be a number greater than 0');
26
+ }
27
+
28
+ this.#maxSize = options.maxSize;
29
+ this.#maxAge = options.maxAge || Number.POSITIVE_INFINITY;
30
+ this.#onEviction = options.onEviction;
31
+ }
32
+
33
+ // For tests.
34
+ get __oldCache() {
35
+ return this.#oldCache;
36
+ }
37
+
38
+ #emitEvictions(cache) {
39
+ if (typeof this.#onEviction !== 'function') {
40
+ return;
41
+ }
42
+
43
+ for (const [key, item] of cache) {
44
+ this.#onEviction(key, item.value);
45
+ }
46
+ }
47
+
48
+ #deleteIfExpired(key, item) {
49
+ if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
50
+ if (typeof this.#onEviction === 'function') {
51
+ this.#onEviction(key, item.value);
52
+ }
53
+
54
+ return this.delete(key);
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ #getOrDeleteIfExpired(key, item) {
61
+ const deleted = this.#deleteIfExpired(key, item);
62
+ if (deleted === false) {
63
+ return item.value;
64
+ }
65
+ }
66
+
67
+ #getItemValue(key, item) {
68
+ return item.expiry ? this.#getOrDeleteIfExpired(key, item) : item.value;
69
+ }
70
+
71
+ #peek(key, cache) {
72
+ const item = cache.get(key);
73
+ return this.#getItemValue(key, item);
74
+ }
75
+
76
+ #set(key, value) {
77
+ this.#cache.set(key, value);
78
+ this.#size++;
79
+
80
+ if (this.#size >= this.#maxSize) {
81
+ this.#size = 0;
82
+ this.#emitEvictions(this.#oldCache);
83
+ this.#oldCache = this.#cache;
84
+ this.#cache = new Map();
85
+ }
86
+ }
87
+
88
+ #moveToRecent(key, item) {
89
+ this.#oldCache.delete(key);
90
+ this.#set(key, item);
91
+ }
92
+
93
+ * #entriesAscending() {
94
+ for (const item of this.#oldCache) {
95
+ const [key, value] = item;
96
+ if (!this.#cache.has(key)) {
97
+ const deleted = this.#deleteIfExpired(key, value);
98
+ if (deleted === false) {
99
+ yield item;
100
+ }
101
+ }
102
+ }
103
+
104
+ for (const item of this.#cache) {
105
+ const [key, value] = item;
106
+ const deleted = this.#deleteIfExpired(key, value);
107
+ if (deleted === false) {
108
+ yield item;
109
+ }
110
+ }
111
+ }
112
+
113
+ get(key) {
114
+ if (this.#cache.has(key)) {
115
+ const item = this.#cache.get(key);
116
+ return this.#getItemValue(key, item);
117
+ }
118
+
119
+ if (this.#oldCache.has(key)) {
120
+ const item = this.#oldCache.get(key);
121
+ if (this.#deleteIfExpired(key, item) === false) {
122
+ this.#moveToRecent(key, item);
123
+ return item.value;
124
+ }
125
+ }
126
+ }
127
+
128
+ set(key, value, {maxAge = this.#maxAge} = {}) {
129
+ const expiry = typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY
130
+ ? (Date.now() + maxAge)
131
+ : undefined;
132
+
133
+ if (this.#cache.has(key)) {
134
+ this.#cache.set(key, {
135
+ value,
136
+ expiry,
137
+ });
138
+ } else {
139
+ this.#set(key, {value, expiry});
140
+ }
141
+
142
+ return this;
143
+ }
144
+
145
+ has(key) {
146
+ if (this.#cache.has(key)) {
147
+ return !this.#deleteIfExpired(key, this.#cache.get(key));
148
+ }
149
+
150
+ if (this.#oldCache.has(key)) {
151
+ return !this.#deleteIfExpired(key, this.#oldCache.get(key));
152
+ }
153
+
154
+ return false;
155
+ }
156
+
157
+ peek(key) {
158
+ if (this.#cache.has(key)) {
159
+ return this.#peek(key, this.#cache);
160
+ }
161
+
162
+ if (this.#oldCache.has(key)) {
163
+ return this.#peek(key, this.#oldCache);
164
+ }
165
+ }
166
+
167
+ expiresIn(key) {
168
+ const item = this.#cache.get(key) ?? this.#oldCache.get(key);
169
+ if (item) {
170
+ return item.expiry ? item.expiry - Date.now() : Number.POSITIVE_INFINITY;
171
+ }
172
+ }
173
+
174
+ delete(key) {
175
+ const deleted = this.#cache.delete(key);
176
+ if (deleted) {
177
+ this.#size--;
178
+ }
179
+
180
+ return this.#oldCache.delete(key) || deleted;
181
+ }
182
+
183
+ clear() {
184
+ this.#cache.clear();
185
+ this.#oldCache.clear();
186
+ this.#size = 0;
187
+ }
188
+
189
+ resize(newSize) {
190
+ if (!(newSize && newSize > 0)) {
191
+ throw new TypeError('`maxSize` must be a number greater than 0');
192
+ }
193
+
194
+ const items = [...this.#entriesAscending()];
195
+ const removeCount = items.length - newSize;
196
+ if (removeCount < 0) {
197
+ this.#cache = new Map(items);
198
+ this.#oldCache = new Map();
199
+ this.#size = items.length;
200
+ } else {
201
+ if (removeCount > 0) {
202
+ this.#emitEvictions(items.slice(0, removeCount));
203
+ }
204
+
205
+ this.#oldCache = new Map(items.slice(removeCount));
206
+ this.#cache = new Map();
207
+ this.#size = 0;
208
+ }
209
+
210
+ this.#maxSize = newSize;
211
+ }
212
+
213
+ evict(count = 1) {
214
+ const requested = Number(count);
215
+ if (!requested || requested <= 0) {
216
+ return;
217
+ }
218
+
219
+ const items = [...this.#entriesAscending()];
220
+ const evictCount = Math.trunc(Math.min(requested, Math.max(items.length - 1, 0)));
221
+ if (evictCount <= 0) {
222
+ return;
223
+ }
224
+
225
+ this.#emitEvictions(items.slice(0, evictCount));
226
+ this.#oldCache = new Map(items.slice(evictCount));
227
+ this.#cache = new Map();
228
+ this.#size = 0;
229
+ }
230
+
231
+ * keys() {
232
+ for (const [key] of this) {
233
+ yield key;
234
+ }
235
+ }
236
+
237
+ * values() {
238
+ for (const [, value] of this) {
239
+ yield value;
240
+ }
241
+ }
242
+
243
+ * [Symbol.iterator]() {
244
+ for (const item of this.#cache) {
245
+ const [key, value] = item;
246
+ const deleted = this.#deleteIfExpired(key, value);
247
+ if (deleted === false) {
248
+ yield [key, value.value];
249
+ }
250
+ }
251
+
252
+ for (const item of this.#oldCache) {
253
+ const [key, value] = item;
254
+ if (!this.#cache.has(key)) {
255
+ const deleted = this.#deleteIfExpired(key, value);
256
+ if (deleted === false) {
257
+ yield [key, value.value];
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ * entriesDescending() {
264
+ let items = [...this.#cache];
265
+ for (let i = items.length - 1; i >= 0; --i) {
266
+ const item = items[i];
267
+ const [key, value] = item;
268
+ const deleted = this.#deleteIfExpired(key, value);
269
+ if (deleted === false) {
270
+ yield [key, value.value];
271
+ }
272
+ }
273
+
274
+ items = [...this.#oldCache];
275
+ for (let i = items.length - 1; i >= 0; --i) {
276
+ const item = items[i];
277
+ const [key, value] = item;
278
+ if (!this.#cache.has(key)) {
279
+ const deleted = this.#deleteIfExpired(key, value);
280
+ if (deleted === false) {
281
+ yield [key, value.value];
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ * entriesAscending() {
288
+ for (const [key, value] of this.#entriesAscending()) {
289
+ yield [key, value.value];
290
+ }
291
+ }
292
+
293
+ get size() {
294
+ if (!this.#size) {
295
+ return this.#oldCache.size;
296
+ }
297
+
298
+ let oldCacheSize = 0;
299
+ for (const key of this.#oldCache.keys()) {
300
+ if (!this.#cache.has(key)) {
301
+ oldCacheSize++;
302
+ }
303
+ }
304
+
305
+ return Math.min(this.#size + oldCacheSize, this.#maxSize);
306
+ }
307
+
308
+ get maxSize() {
309
+ return this.#maxSize;
310
+ }
311
+
312
+ get maxAge() {
313
+ return this.#maxAge;
314
+ }
315
+
316
+ entries() {
317
+ return this.entriesAscending();
318
+ }
319
+
320
+ forEach(callbackFunction, thisArgument = this) {
321
+ for (const [key, value] of this.entriesAscending()) {
322
+ callbackFunction.call(thisArgument, value, key, this);
323
+ }
324
+ }
325
+
326
+ get [Symbol.toStringTag]() {
327
+ return 'QuickLRU';
328
+ }
329
+
330
+ toString() {
331
+ return `QuickLRU(${this.size}/${this.maxSize})`;
332
+ }
333
+
334
+ [Symbol.for('nodejs.util.inspect.custom')]() {
335
+ return this.toString();
336
+ }
337
+ }
338
+
9
339
  var city2geonameid = {
10
340
  "AD-Andorra La Vella":3041563,
11
341
  "AE-Abu Dhabi":292968,
@@ -511,7 +841,7 @@ function munge(s) {
511
841
  }
512
842
 
513
843
  // DO NOT EDIT THIS AUTO-GENERATED FILE!
514
- const version = '5.4.1';
844
+ const version = '5.5.1';
515
845
 
516
846
  const GEONAME_SQL = `SELECT
517
847
  g.name as name,
@@ -644,10 +974,10 @@ class GeoDb {
644
974
  this.geonamesDb = new Database(geonamesFilename, {fileMustExist: true});
645
975
  this.zipStmt = this.zipsDb.prepare(ZIPCODE_SQL);
646
976
  /** @type {Map<string, Location>} */
647
- this.zipCache = new Map();
977
+ this.zipCache = new QuickLRU({maxSize: 150});
648
978
  this.geonamesStmt = this.geonamesDb.prepare(GEONAME_SQL);
649
979
  /** @type {Map<number, Location>} */
650
- this.geonamesCache = new Map();
980
+ this.geonamesCache = new QuickLRU({maxSize: 750});
651
981
  /** @type {Map<string, number>} */
652
982
  this.legacyCities = new Map();
653
983
  for (const [name, id] of Object.entries(city2geonameid)) {
@@ -1015,6 +1345,7 @@ class GeoDb {
1015
1345
  const start = Date.now();
1016
1346
  const stmt = this.zipsDb.prepare(ZIPCODE_ALL_SQL);
1017
1347
  const rows = stmt.all();
1348
+ this.zipCache = new Map(); // replace QuickLRU
1018
1349
  for (const row of rows) {
1019
1350
  const location = this.makeZipLocation(row);
1020
1351
  this.zipCache.set(row.ZipCode, location);
@@ -1028,6 +1359,7 @@ class GeoDb {
1028
1359
  const start = Date.now();
1029
1360
  const stmt = this.geonamesDb.prepare(GEONAME_ALL_SQL);
1030
1361
  const rows = stmt.all();
1362
+ this.geonamesCache = new Map(); // replace QuickLRU
1031
1363
  for (const row of rows) {
1032
1364
  const location = this.makeGeonameLocation(row.geonameid, row);
1033
1365
  this.geonamesCache.set(row.geonameid, location);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hebcal/geo-sqlite",
3
- "version": "5.4.1",
3
+ "version": "5.5.1",
4
4
  "author": "Michael J. Radwin (https://github.com/mjradwin)",
5
5
  "keywords": [
6
6
  "hebcal"
@@ -40,6 +40,7 @@
40
40
  "better-sqlite3": "^12.5.0",
41
41
  "pino": "^10.1.0",
42
42
  "pino-pretty": "^13.1.2",
43
+ "quick-lru": "^7.3.0",
43
44
  "transliteration": "^2.3.5"
44
45
  },
45
46
  "scripts": {
package/readme.txt DELETED
@@ -1,141 +0,0 @@
1
-
2
- Readme for GeoNames Gazetteer extract files
3
-
4
- ============================================================================================================
5
-
6
- This work is licensed under a Creative Commons Attribution 4.0 License,
7
- see https://creativecommons.org/licenses/by/4.0/
8
- The Data is provided "as is" without warranty or any representation of accuracy, timeliness or completeness.
9
-
10
- The data format is tab-delimited text in utf8 encoding.
11
-
12
-
13
- Files :
14
- -------
15
- XX.zip : features for country with iso code XX, see 'geoname' table for columns. 'no-country' for features not belonging to a country.
16
- allCountries.zip : all countries combined in one file, see 'geoname' table for columns
17
- cities500.zip : all cities with a population > 500 or seats of adm div down to PPLA4 (ca 185.000), see 'geoname' table for columns
18
- cities1000.zip : all cities with a population > 1000 or seats of adm div down to PPLA3 (ca 130.000), see 'geoname' table for columns
19
- cities5000.zip : all cities with a population > 5000 or PPLA (ca 50.000), see 'geoname' table for columns
20
- cities15000.zip : all cities with a population > 15000 or capitals (ca 25.000), see 'geoname' table for columns
21
- alternateNamesV2.zip : alternate names with language codes and geonameId, file with iso language codes, with new columns from and to
22
- alternateNames.zip : obsolete use V2, this file does not have the new columns to and from and will be removed in the future
23
- admin1CodesASCII.txt : names in English for admin divisions. Columns: code, name, name ascii, geonameid
24
- admin2Codes.txt : names for administrative subdivision 'admin2 code' (UTF8), Format : concatenated codes <tab>name <tab> asciiname <tab> geonameId
25
- iso-languagecodes.txt : iso 639 language codes, as used for alternate names in file alternateNames.zip
26
- featureCodes.txt : name and description for feature classes and feature codes
27
- timeZones.txt : countryCode, timezoneId, gmt offset on 1st of January, dst offset to gmt on 1st of July (of the current year), rawOffset without DST
28
- countryInfo.txt : country information : iso codes, fips codes, languages, capital ,...
29
- see the geonames webservices for additional country information,
30
- bounding box : http://api.geonames.org/countryInfo?
31
- country names in different languages : http:/api.geonames.org/countryInfoCSV?lang=it
32
- modifications-<date>.txt : all records modified on the previous day, the date is in yyyy-MM-dd format. You can use this file to daily synchronize your own geonames database.
33
- deletes-<date>.txt : all records deleted on the previous day, format : geonameId <tab> name <tab> comment.
34
-
35
- alternateNamesModifications-<date>.txt : all alternate names modified on the previous day,
36
- alternateNamesDeletes-<date>.txt : all alternate names deleted on the previous day, format : alternateNameId <tab> geonameId <tab> name <tab> comment.
37
- userTags.zip : user tags , format : geonameId <tab> tag.
38
- hierarchy.zip : parentId, childId, type. The type 'ADM' stands for the admin hierarchy modeled by the admin1-4 codes. The other entries are entered with the user interface. The relation toponym-adm hierarchy is not included in the file, it can instead be built from the admincodes of the toponym.
39
- adminCode5.zip : the new adm5 column is not yet exported in the other files (in order to not break import scripts). Instead it is availabe as separate file.
40
- columns: geonameId,adm5code
41
-
42
- The main 'geoname' table has the following fields :
43
- ---------------------------------------------------
44
- geonameid : integer id of record in geonames database
45
- name : name of geographical point (utf8) varchar(200)
46
- asciiname : name of geographical point in plain ascii characters, varchar(200)
47
- alternatenames : alternatenames, comma separated, ascii names automatically transliterated, convenience attribute from alternatename table, varchar(10000)
48
- latitude : latitude in decimal degrees (wgs84)
49
- longitude : longitude in decimal degrees (wgs84)
50
- feature class : see http://www.geonames.org/export/codes.html, char(1)
51
- feature code : see http://www.geonames.org/export/codes.html, varchar(10)
52
- country code : ISO-3166 2-letter country code, 2 characters
53
- cc2 : alternate country codes, comma separated, ISO-3166 2-letter country code, 200 characters
54
- admin1 code : fipscode (subject to change to iso code), see exceptions below, see file admin1Codes.txt for display names of this code; varchar(20)
55
- admin2 code : code for the second administrative division, a county in the US, see file admin2Codes.txt; varchar(80)
56
- admin3 code : code for third level administrative division, varchar(20)
57
- admin4 code : code for fourth level administrative division, varchar(20)
58
- population : bigint (8 byte int)
59
- elevation : in meters, integer
60
- dem : digital elevation model, srtm3 or gtopo30, average elevation of 3''x3'' (ca 90mx90m) or 30''x30'' (ca 900mx900m) area in meters, integer. srtm processed by cgiar/ciat.
61
- timezone : the iana timezone id (see file timeZone.txt) varchar(40)
62
- modification date : date of last modification in yyyy-MM-dd format
63
-
64
-
65
- AdminCodes:
66
- Most adm1 are FIPS codes. ISO codes are used for US, CH, BE and ME. UK and Greece are using an additional level between country and fips code. The code '00' stands for general features where no specific adm1 code is defined.
67
- The corresponding admin feature is found with the same countrycode and adminX codes and the respective feature code ADMx.
68
-
69
-
70
-
71
- The table 'alternate names' :
72
- -----------------------------
73
- alternateNameId : the id of this alternate name, int
74
- geonameid : geonameId referring to id in table 'geoname', int
75
- isolanguage : iso 639 language code 2- or 3-characters, optionally followed by a hyphen and a countrycode for country specific variants (ex:zh-CN) or by a variant name (ex: zh-Hant); 4-characters 'post' for postal codes and 'iata','icao' and faac for airport codes, fr_1793 for French Revolution names, abbr for abbreviation, link to a website (mostly to wikipedia), wkdt for the wikidataid, varchar(7)
76
- alternate name : alternate name or name variant, varchar(400)
77
- isPreferredName : '1', if this alternate name is an official/preferred name
78
- isShortName : '1', if this is a short name like 'California' for 'State of California'
79
- isColloquial : '1', if this alternate name is a colloquial or slang term. Example: 'Big Apple' for 'New York'.
80
- isHistoric : '1', if this alternate name is historic and was used in the past. Example 'Bombay' for 'Mumbai'.
81
- from : from period when the name was used
82
- to : to period when the name was used
83
-
84
- Remark : the field 'alternatenames' in the table 'geoname' is a short version of the 'alternatenames' table without links and postal codes but with ascii transliterations. You probably don't need both.
85
- If you don't need to know the language of a name variant, the field 'alternatenames' will be sufficient. If you need to know the language
86
- of a name variant, then you will need to load the table 'alternatenames' and you can drop the column in the geoname table.
87
-
88
-
89
-
90
-
91
- Boundaries:
92
- Simplified country boundaries are available in two slightly different formats:
93
- shapes_simplified_low:
94
- geonameId: The geonameId of the feature
95
- geoJson: The boundary in geoJson format
96
-
97
- shapes_simplified_low.json:
98
- similar to the abovementioned file, but fully in geojson format. The geonameId is a feature property in the geojson string.
99
-
100
-
101
- Statistics on the number of features per country and the feature class and code distributions : http://www.geonames.org/statistics/
102
-
103
-
104
- Continent codes :
105
- AF : Africa geonameId=6255146
106
- AS : Asia geonameId=6255147
107
- EU : Europe geonameId=6255148
108
- NA : North America geonameId=6255149
109
- OC : Oceania geonameId=6255151
110
- SA : South America geonameId=6255150
111
- AN : Antarctica geonameId=6255152
112
-
113
-
114
- feature classes:
115
- A: country, state, region,...
116
- H: stream, lake, ...
117
- L: parks,area, ...
118
- P: city, village,...
119
- R: road, railroad
120
- S: spot, building, farm
121
- T: mountain,hill,rock,...
122
- U: undersea
123
- V: forest,heath,...
124
-
125
-
126
- If you find errors or miss important places, please do use the wiki-style edit interface on our website
127
- https://www.geonames.org to correct inaccuracies and to add new records.
128
- Thanks in the name of the geonames community for your valuable contribution.
129
-
130
- Data Sources:
131
- https://www.geonames.org/datasources/
132
-
133
-
134
- More Information is also available in the geonames faq :
135
-
136
- https://forum.geonames.org/gforum/forums/show/6.page
137
-
138
- The forum : https://forum.geonames.org
139
-
140
- or the google group : https://groups.google.com/group/geonames
141
-