catastrogps 1.1.0 → 1.2.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.
package/dist/index.mjs CHANGED
@@ -25,8 +25,24 @@ var NotFoundError = class extends CatastroGPSError {
25
25
  var ValidationError = class extends CatastroGPSError {
26
26
  name = "ValidationError";
27
27
  };
28
+ var PlaceNameError = class extends ValidationError {
29
+ name = "PlaceNameError";
30
+ get location() {
31
+ const details = this.details;
32
+ return details?.location;
33
+ }
34
+ };
28
35
  var CoverageError = class extends CatastroGPSError {
29
36
  name = "CoverageError";
37
+ get country() {
38
+ return this.details?.country;
39
+ }
40
+ get feature() {
41
+ return this.details?.feature;
42
+ }
43
+ get supportedCountries() {
44
+ return this.details?.supportedCountries ?? [];
45
+ }
30
46
  };
31
47
  var AmbiguousReferenceError = class extends CatastroGPSError {
32
48
  name = "AmbiguousReferenceError";
@@ -37,6 +53,10 @@ var AmbiguousReferenceError = class extends CatastroGPSError {
37
53
  };
38
54
  var QuotaExceededError = class extends CatastroGPSError {
39
55
  name = "QuotaExceededError";
56
+ quota;
57
+ get prepaidBalanceEmpty() {
58
+ return this.quota?.overage?.enabled === true;
59
+ }
40
60
  };
41
61
  var RateLimitError = class extends CatastroGPSError {
42
62
  name = "RateLimitError";
@@ -55,14 +75,19 @@ var TimeoutError = class extends CatastroGPSError {
55
75
  var NetworkError = class extends CatastroGPSError {
56
76
  name = "NetworkError";
57
77
  };
58
- function errorFromResponse(status, body, retryAfter) {
78
+ function errorFromResponse(status, body, retryAfter, quota) {
59
79
  const code = typeof body.code === "string" ? body.code : void 0;
60
80
  const message = typeof body.error === "string" && body.error || typeof body.message === "string" && body.message || `HTTP ${status}`;
61
81
  const details = body.data ?? body.parsed;
62
82
  const options = { status, code, details, body };
63
83
  if (code === "CNV_AMBIGUOUS" || status === 300) return new AmbiguousReferenceError(message, options);
64
84
  if (code === "CNV_COVERAGE") return new CoverageError(message, options);
65
- if (code === "KEY_AUTH_004") return new QuotaExceededError(message, options);
85
+ if (code === "CNV_PLACE_NAME") return new PlaceNameError(message, options);
86
+ if (code === "KEY_AUTH_004") {
87
+ const error = new QuotaExceededError(message, options);
88
+ error.quota = quota;
89
+ return error;
90
+ }
66
91
  if (status === 429) {
67
92
  const error = new RateLimitError(message, options);
68
93
  error.retryAfterSeconds = retryAfter;
@@ -100,7 +125,75 @@ var ExportResource = class {
100
125
  }
101
126
  };
102
127
 
128
+ // src/types.ts
129
+ var COUNTRY_CODES = [
130
+ "ES",
131
+ "PV",
132
+ "NA",
133
+ "PT",
134
+ "FR",
135
+ "IT",
136
+ "DE",
137
+ "AT",
138
+ "CH",
139
+ "LI",
140
+ "BE",
141
+ "NL",
142
+ "LU",
143
+ "PL",
144
+ "CZ",
145
+ "SK",
146
+ "SI",
147
+ "HR",
148
+ "BG",
149
+ "GR",
150
+ "CY",
151
+ "DK",
152
+ "SE",
153
+ "NO",
154
+ "FI",
155
+ "IS",
156
+ "EE",
157
+ "LV",
158
+ "LT",
159
+ "IE",
160
+ "UK"
161
+ ];
162
+ var ENRICHMENT_COUNTRY_CODES = ["ES", "PV", "NA", "PT", "FR", "IT", "DE"];
163
+ var ADDRESS_COUNTRY_CODES = [
164
+ "ES",
165
+ "FR",
166
+ "IT",
167
+ "DE",
168
+ "AT",
169
+ "NL",
170
+ "BE",
171
+ "PL",
172
+ "CH",
173
+ "CZ",
174
+ "DK",
175
+ "NO",
176
+ "FI",
177
+ "EE",
178
+ "LV",
179
+ "LT",
180
+ "SI",
181
+ "SK",
182
+ "BG",
183
+ "GR",
184
+ "CY",
185
+ "LU",
186
+ "LI",
187
+ "IS",
188
+ "IE",
189
+ "UK",
190
+ "PT"
191
+ ];
192
+ var UNITS_COUNTRY_CODES = ["ES", "PV", "NA"];
193
+
103
194
  // src/resources/parcels.ts
195
+ var MIN_COMPARE = 2;
196
+ var MAX_COMPARE = 3;
104
197
  var ParcelsResource = class {
105
198
  constructor(client) {
106
199
  this.client = client;
@@ -133,7 +226,46 @@ var ParcelsResource = class {
133
226
  });
134
227
  return data.agro ?? data;
135
228
  }
229
+ async terrain(reference, options = {}) {
230
+ return this.client.request("GET", parcelPath(reference, "/terrain"), { query: locatedQuery(options) });
231
+ }
232
+ async groundMotion(reference, options = {}) {
233
+ return this.client.request("GET", parcelPath(reference, "/ground-motion"), { query: locatedQuery(options) });
234
+ }
235
+ async market(reference, options = {}) {
236
+ return this.client.request("GET", parcelPath(reference, "/market"), { query: { country: options.country } });
237
+ }
238
+ async score(reference, options = {}) {
239
+ return this.client.request("GET", parcelPath(reference, "/score"), { query: { country: options.country } });
240
+ }
241
+ async valueHistory(reference, options = {}) {
242
+ return this.client.request("GET", parcelPath(reference, "/value-history"), {
243
+ query: { country: options.country }
244
+ });
245
+ }
246
+ async compare(parcels) {
247
+ if (parcels.length < MIN_COMPARE || parcels.length > MAX_COMPARE) {
248
+ throw new CatastroGPSError(`compare takes ${MIN_COMPARE} or ${MAX_COMPARE} parcels`);
249
+ }
250
+ const parcelas = parcels.map((parcel) => {
251
+ if (!parcel.reference?.trim()) throw new CatastroGPSError("reference must not be empty");
252
+ if (!ENRICHMENT_COUNTRY_CODES.includes(parcel.country)) {
253
+ throw new CatastroGPSError(`country must be one of ${ENRICHMENT_COUNTRY_CODES.join(", ")}`);
254
+ }
255
+ return { ref_catastral: parcel.reference.trim(), country: parcel.country };
256
+ });
257
+ return this.client.request("POST", "/api/catastro/compare", { body: { parcelas } });
258
+ }
136
259
  };
260
+ function locatedQuery(options) {
261
+ const hasLat = options.lat !== void 0;
262
+ const hasLng = options.lng !== void 0;
263
+ if (hasLat !== hasLng) throw new CatastroGPSError("lat and lng go together");
264
+ if (hasLat && (!Number.isFinite(options.lat) || !Number.isFinite(options.lng))) {
265
+ throw new CatastroGPSError("lat and lng must be finite numbers");
266
+ }
267
+ return { country: options.country, lat: options.lat, lng: options.lng };
268
+ }
137
269
  function parcelPath(reference, suffix = "") {
138
270
  if (!reference.trim()) throw new CatastroGPSError("reference must not be empty");
139
271
  return `/api/catastro/${encodeURIComponent(reference.trim())}${suffix}`;
@@ -151,6 +283,7 @@ var FincasResource = class {
151
283
  if (!params.q?.trim() && !params.street?.trim()) throw new CatastroGPSError("q or street is required");
152
284
  return this.client.request("GET", "/api/search/address/candidates", {
153
285
  query: {
286
+ country: params.country,
154
287
  q: params.q?.trim(),
155
288
  street: params.street?.trim(),
156
289
  number: params.number,
@@ -167,7 +300,7 @@ var FincasResource = class {
167
300
  for (let count = 0; count < MAX_PAGES; count += 1) {
168
301
  const previous = previousByCursor.get(cursor ?? "");
169
302
  const response = await this.client.conditionalRequest("GET", path, {
170
- query: { cursor },
303
+ query: { cursor, country: options.country },
171
304
  ifNoneMatch: previous?.etag
172
305
  });
173
306
  const data = response.notModified && previous ? previous.data : response.data;
@@ -205,7 +338,7 @@ var FincasResource = class {
205
338
  };
206
339
 
207
340
  // src/version.ts
208
- var VERSION = "1.1.0";
341
+ var VERSION = "1.2.1";
209
342
 
210
343
  // src/client.ts
211
344
  var DEFAULT_BASE_URL = "https://api.catastrogps.es";
@@ -300,17 +433,22 @@ var CatastroGPS = class {
300
433
  } finally {
301
434
  clearTimeout(timer);
302
435
  }
303
- this.captureQuota(response.headers);
436
+ const quota = this.captureQuota(response.headers);
304
437
  const etag = response.headers.get("ETag") ?? void 0;
305
- if (response.status === NOT_MODIFIED) return { etag: etag ?? options.ifNoneMatch, notModified: true };
438
+ if (response.status === NOT_MODIFIED) return { etag: etag ?? options.ifNoneMatch, notModified: true, quota };
306
439
  if (!response.ok) {
307
440
  const body2 = await readJson(response);
308
441
  const retryAfter = Number(response.headers.get("Retry-After"));
309
- throw errorFromResponse(response.status, body2, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0);
442
+ throw errorFromResponse(
443
+ response.status,
444
+ body2,
445
+ Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0,
446
+ readQuota(response.headers)
447
+ );
310
448
  }
311
- if (options.raw) return { data: await response.arrayBuffer(), etag, notModified: false };
449
+ if (options.raw) return { data: await response.arrayBuffer(), etag, notModified: false, quota };
312
450
  const body = await readJson(response);
313
- return { data: "data" in body ? body.data : body, etag, notModified: false };
451
+ return { data: "data" in body ? body.data : body, etag, notModified: false, quota };
314
452
  }
315
453
  buildUrl(path, query) {
316
454
  const url = new URL(`${this.baseUrl}${path}`);
@@ -334,20 +472,47 @@ var CatastroGPS = class {
334
472
  captureQuota(headers) {
335
473
  const quota = readQuota(headers);
336
474
  if (quota) this.lastQuota = quota;
475
+ return quota;
337
476
  }
338
477
  };
339
478
  function readQuota(headers) {
340
- const plan = headers.get("X-Quota-Tier") ?? void 0;
341
- const family = headers.has("X-Quota-Limit") ? "X-Quota" : plan ? "X-RateLimit" : void 0;
342
- if (!family) return void 0;
343
- const limit = headers.get(`${family}-Limit`);
344
- const remaining = headers.get(`${family}-Remaining`);
345
- return {
346
- plan,
347
- limit: limit === null ? void 0 : Number(limit),
348
- remaining: remaining === null ? void 0 : Number(remaining),
349
- resetsAt: headers.get(`${family}-Reset`) ?? void 0
350
- };
479
+ const quota = {};
480
+ const plan = headers.get("X-Quota-Tier");
481
+ if (plan !== null) quota.plan = plan;
482
+ const limit = numberHeader(headers, "X-Quota-Limit");
483
+ if (limit !== void 0) quota.limit = limit;
484
+ const remaining = numberHeader(headers, "X-Quota-Remaining");
485
+ if (remaining !== void 0) quota.remaining = remaining;
486
+ const resetsAt = headers.get("X-Quota-Reset");
487
+ if (resetsAt !== null) quota.resetsAt = resetsAt;
488
+ if (headers.get("X-Quota-Overage")?.toLowerCase() === "on") {
489
+ quota.overage = {
490
+ enabled: true,
491
+ balance: numberHeader(headers, "X-Quota-Balance"),
492
+ unitPrice: numberHeader(headers, "X-Quota-Overage-Unit-Price"),
493
+ remainingUnits: numberHeader(headers, "X-Quota-Overage-Remaining")
494
+ };
495
+ }
496
+ if (headers.has("X-RateLimit-Limit") || headers.has("X-RateLimit-Remaining")) {
497
+ quota.rateLimit = {
498
+ limit: numberHeader(headers, "X-RateLimit-Limit"),
499
+ remaining: numberHeader(headers, "X-RateLimit-Remaining"),
500
+ resetSeconds: numberHeader(headers, "X-RateLimit-Reset")
501
+ };
502
+ }
503
+ if (headers.has("X-Quota-Finca-Cap")) {
504
+ quota.fincaCap = {
505
+ cap: numberHeader(headers, "X-Quota-Finca-Cap"),
506
+ used: numberHeader(headers, "X-Quota-Finca-Cap-Used")
507
+ };
508
+ }
509
+ return Object.keys(quota).length > 0 ? quota : void 0;
510
+ }
511
+ function numberHeader(headers, name) {
512
+ const raw = headers.get(name);
513
+ if (raw === null || raw.trim() === "") return void 0;
514
+ const value = Number(raw);
515
+ return Number.isFinite(value) ? value : void 0;
351
516
  }
352
517
  async function readJson(response) {
353
518
  try {
@@ -364,43 +529,8 @@ function readEnv(name) {
364
529
  function sleep(ms) {
365
530
  return new Promise((resolve) => setTimeout(resolve, ms));
366
531
  }
367
-
368
- // src/types.ts
369
- var COUNTRY_CODES = [
370
- "ES",
371
- "PV",
372
- "NA",
373
- "PT",
374
- "FR",
375
- "IT",
376
- "DE",
377
- "AT",
378
- "CH",
379
- "LI",
380
- "BE",
381
- "NL",
382
- "LU",
383
- "PL",
384
- "CZ",
385
- "SK",
386
- "SI",
387
- "HR",
388
- "BG",
389
- "GR",
390
- "CY",
391
- "DK",
392
- "SE",
393
- "NO",
394
- "FI",
395
- "IS",
396
- "EE",
397
- "LV",
398
- "LT",
399
- "IE",
400
- "UK"
401
- ];
402
- var ENRICHMENT_COUNTRY_CODES = ["ES", "PV", "NA", "PT", "FR", "IT", "DE"];
403
532
  export {
533
+ ADDRESS_COUNTRY_CODES,
404
534
  AmbiguousReferenceError,
405
535
  AuthenticationError,
406
536
  COUNTRY_CODES,
@@ -414,11 +544,14 @@ export {
414
544
  ForbiddenError,
415
545
  NetworkError,
416
546
  NotFoundError,
547
+ PlaceNameError,
417
548
  QuotaExceededError,
418
549
  RateLimitError,
419
550
  ServerError,
420
551
  ServiceUnavailableError,
421
552
  TimeoutError,
553
+ UNITS_COUNTRY_CODES,
422
554
  VERSION,
423
- ValidationError
555
+ ValidationError,
556
+ readQuota
424
557
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "catastrogps",
3
- "version": "1.1.0",
4
- "description": "Official JavaScript/TypeScript client for the Catastro GPS API: cadastral parcels in 31 European countries and regions by reference, coordinates or Spanish address",
3
+ "version": "1.2.1",
4
+ "description": "Official JavaScript/TypeScript client for the Catastro GPS API: parcels from 29 European countries (31 official cadastres, including the Basque Country and Navarre) by reference, coordinates or address; building units, terrain, ground motion, solar and more",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "test": "vitest run",
31
+ "test:coverage": "vitest run --coverage",
31
32
  "prepublishOnly": "npm run typecheck && npm test && npm run build"
32
33
  },
33
34
  "keywords": [
@@ -48,6 +49,9 @@
48
49
  "germany",
49
50
  "real-estate",
50
51
  "proptech",
52
+ "copernicus",
53
+ "natura-2000",
54
+ "ground-motion",
51
55
  "sdk"
52
56
  ],
53
57
  "author": "The Hidden Panda <soporte@catastrogps.es>",
@@ -57,10 +61,11 @@
57
61
  "email": "soporte@catastrogps.es"
58
62
  },
59
63
  "devDependencies": {
64
+ "@types/node": "^22.0.0",
65
+ "@vitest/coverage-v8": "^3.2.7",
60
66
  "tsup": "^8.0.0",
61
67
  "typescript": "^5.4.0",
62
- "vitest": "^3.0.0",
63
- "@types/node": "^22.0.0"
68
+ "vitest": "^3.0.0"
64
69
  },
65
70
  "engines": {
66
71
  "node": ">=18.0.0"