catastrogps 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -55,8 +55,40 @@ guess.candidates;
55
55
  client.lastQuota;
56
56
  ```
57
57
 
58
+ `client.lastQuota` is the monthly quota of your plan as of the last response: `{ plan, limit, remaining, resetsAt }`, read from the `X-Quota-Tier`, `X-Quota-Limit`, `X-Quota-Remaining` and `X-Quota-Reset` headers. The `X-RateLimit-*` headers are a different thing: a per-key burst limit per minute that depends on your plan (Free 10, Developer 60, Startup 120, Growth 300; a global per-IP guard also applies) that the client handles by retrying.
59
+
58
60
  Responses are the API's `data` object, with the field names the API uses (`refCatastral`, `municipio`, `superficieParcela`…). See the [API reference](https://www.catastrogps.es/developers).
59
61
 
62
+ ## Importar una comunidad de propietarios (Spain)
63
+
64
+ Address → finca (14-character reference) → every unit, with use, area, participation coefficient, stair, floor and door.
65
+
66
+ ```ts
67
+ const { candidatos } = await client.searchAddressCandidates('Avenida José Rodríguez de la Borbolla Camoyán 10, Dos Hermanas');
68
+ const finca = candidatos[0];
69
+ finca.refCatastral;
70
+ finca.confianza;
71
+ finca.pais;
72
+
73
+ const comunidad = await client.getUnits(finca.refCatastral);
74
+ comunidad.totalUnidadesFinca;
75
+ for (const u of comunidad.unidades) {
76
+ console.log(u.refCatastral, u.uso, u.superficie, u.participacion, u.escalera, u.planta, u.puerta);
77
+ }
78
+ comunidad.dataSource;
79
+ comunidad.dataDate;
80
+ comunidad.attribution;
81
+
82
+ const refreshed = await client.getUnits(finca.refCatastral, { previous: comunidad });
83
+ refreshed.changed;
84
+ ```
85
+
86
+ - `searchAddressCandidates` also takes `{ street, number, municipality, postcode, limit }`. Candidates come ranked by `confianza` (0.75 or more: number and municipality match). It costs 1 quota unit.
87
+ - `getUnits` follows `nextCursor` for you (200 units per page) and costs one quota unit per 50 units, minimum one per page. `iterUnitPages(ref)` yields page by page.
88
+ - Pass the previous result as `previous` to re-import: pages that did not change come back as `304 Not Modified`, cost nothing, and are reused; `changed` tells you if anything moved.
89
+ - Fincas in the Basque Country or Navarra (`pais: 'PV' | 'NA'`) throw `CoverageError`: their foral cadastres are not served per finca.
90
+ - A `ServiceUnavailableError` carries `retryAfterSeconds`; the client already waits for it (up to 60 s) before its retries.
91
+
60
92
  ## Errors
61
93
 
62
94
  Every error is a `CatastroGPSError` with `status`, `code` and `details`. Subclasses let you branch cleanly:
@@ -82,7 +114,7 @@ try {
82
114
 
83
115
  Also available: `AuthenticationError`, `ValidationError`, `RateLimitError`, `ServiceUnavailableError`, `ServerError`, `TimeoutError`, `NetworkError`.
84
116
 
85
- Timeouts, network errors, 429 rate limits and 502/503/504 are retried up to `maxRetries` times (default 2) with exponential backoff. An exhausted monthly quota is never retried. Each attempt that reaches the API counts as a call.
117
+ Timeouts, network errors, 429 rate limits and 502/503/504 are retried up to `maxRetries` times (default 2) with exponential backoff. An exhausted monthly quota is never retried. Only 2xx responses spend quota; failed attempts and `304 Not Modified` are free.
86
118
 
87
119
  ## Options
88
120
 
package/dist/index.d.mts CHANGED
@@ -117,6 +117,110 @@ interface ResolveResult {
117
117
  candidates: ResolveCandidate[];
118
118
  ambiguous: boolean;
119
119
  }
120
+ interface AddressCandidatesQuery {
121
+ q?: string;
122
+ street?: string;
123
+ number?: number;
124
+ municipality?: string;
125
+ postcode?: string;
126
+ limit?: number;
127
+ }
128
+ interface AddressCandidate {
129
+ refCatastral: string;
130
+ pais: 'ES' | 'PV' | 'NA' | string;
131
+ direccion: string;
132
+ numero?: number;
133
+ codigoPostal?: string;
134
+ municipio: string;
135
+ provincia: string;
136
+ latitud: number;
137
+ longitud: number;
138
+ confianza: number;
139
+ coincideNumero: boolean;
140
+ coincideMunicipio: boolean;
141
+ enCopia: boolean;
142
+ direccionCatastro?: string;
143
+ uso?: string;
144
+ viviendas?: number;
145
+ anioConstruccion?: number;
146
+ }
147
+ interface AddressCandidatesResult {
148
+ consulta: {
149
+ texto: string;
150
+ calle?: string;
151
+ numero?: number;
152
+ municipio?: string;
153
+ codigoPostal?: string;
154
+ };
155
+ candidatos: AddressCandidate[];
156
+ attribution: string;
157
+ }
158
+ interface Unit {
159
+ refCatastral: string;
160
+ escalera: string;
161
+ planta: string;
162
+ puerta: string;
163
+ uso: string;
164
+ superficie: number;
165
+ descripcion: string;
166
+ participacion?: number;
167
+ anio?: number;
168
+ direccion?: string;
169
+ }
170
+ interface Construction {
171
+ escalera: string;
172
+ planta: string;
173
+ puerta: string;
174
+ uso: string;
175
+ superficie: number;
176
+ descripcion: string;
177
+ }
178
+ interface UnitsPageData {
179
+ refCatastral: string;
180
+ direccion?: string;
181
+ codigoPostal?: string;
182
+ municipio?: string;
183
+ provincia?: string;
184
+ usoGeneral?: string;
185
+ superficieTotal?: number;
186
+ anioConstruccion?: number;
187
+ participacion?: string;
188
+ totalUnidades: number;
189
+ totalUnidadesFinca: number;
190
+ unidades: Unit[];
191
+ construcciones?: Construction[];
192
+ truncated: boolean;
193
+ nextCursor?: string;
194
+ dataSource: 'clone' | 'catastro' | string;
195
+ dataDate: string;
196
+ attribution: string;
197
+ }
198
+ interface UnitsPage {
199
+ cursor?: string;
200
+ etag?: string;
201
+ notModified: boolean;
202
+ data: UnitsPageData;
203
+ }
204
+ interface UnitsResult {
205
+ refCatastral: string;
206
+ direccion?: string;
207
+ codigoPostal?: string;
208
+ municipio?: string;
209
+ provincia?: string;
210
+ usoGeneral?: string;
211
+ anioConstruccion?: number;
212
+ totalUnidadesFinca: number;
213
+ unidades: Unit[];
214
+ construcciones: Construction[];
215
+ dataSource: string;
216
+ dataDate: string;
217
+ attribution: string;
218
+ changed: boolean;
219
+ pages: UnitsPage[];
220
+ }
221
+ interface GetUnitsOptions {
222
+ previous?: UnitsResult;
223
+ }
120
224
 
121
225
  declare class ExportResource {
122
226
  private readonly client;
@@ -124,6 +228,14 @@ declare class ExportResource {
124
228
  file(reference: string, format: ExportFormat, options?: CountryOption): Promise<ArrayBuffer>;
125
229
  }
126
230
 
231
+ declare class FincasResource {
232
+ private readonly client;
233
+ constructor(client: CatastroGPS);
234
+ searchAddress(query: string | AddressCandidatesQuery): Promise<AddressCandidatesResult>;
235
+ pages(refcat14: string, options?: GetUnitsOptions): AsyncGenerator<UnitsPage>;
236
+ units(refcat14: string, options?: GetUnitsOptions): Promise<UnitsResult>;
237
+ }
238
+
127
239
  interface PointQuery extends CountryOption {
128
240
  lat: number;
129
241
  lng: number;
@@ -147,10 +259,17 @@ interface RequestOptions {
147
259
  query?: Query;
148
260
  body?: unknown;
149
261
  raw?: boolean;
262
+ ifNoneMatch?: string;
263
+ }
264
+ interface ConditionalResponse<T> {
265
+ data?: T;
266
+ etag?: string;
267
+ notModified: boolean;
150
268
  }
151
269
  declare class CatastroGPS {
152
270
  readonly parcels: ParcelsResource;
153
271
  readonly export: ExportResource;
272
+ readonly fincas: FincasResource;
154
273
  lastQuota?: Quota;
155
274
  private readonly apiKey;
156
275
  private readonly baseUrl;
@@ -158,10 +277,14 @@ declare class CatastroGPS {
158
277
  private readonly maxRetries;
159
278
  private readonly fetchImpl;
160
279
  constructor(options?: CatastroGPSOptions);
280
+ searchAddressCandidates(query: string | AddressCandidatesQuery): Promise<AddressCandidatesResult>;
281
+ getUnits(refcat14: string, options?: GetUnitsOptions): Promise<UnitsResult>;
282
+ iterUnitPages(refcat14: string, options?: GetUnitsOptions): AsyncGenerator<UnitsPage>;
161
283
  resolve(text: string, options?: {
162
284
  hint?: string;
163
285
  }): Promise<ResolveResult>;
164
286
  request<T>(method: 'GET' | 'POST', path: string, options?: RequestOptions): Promise<T>;
287
+ conditionalRequest<T>(method: 'GET' | 'POST', path: string, options?: RequestOptions): Promise<ConditionalResponse<T>>;
165
288
  private once;
166
289
  private buildUrl;
167
290
  private shouldRetry;
@@ -169,7 +292,7 @@ declare class CatastroGPS {
169
292
  private captureQuota;
170
293
  }
171
294
 
172
- declare const VERSION = "1.0.0";
295
+ declare const VERSION = "1.1.0";
173
296
 
174
297
  declare class CatastroGPSError extends Error {
175
298
  readonly status?: number;
@@ -215,6 +338,7 @@ declare class RateLimitError extends CatastroGPSError {
215
338
  }
216
339
  declare class ServiceUnavailableError extends CatastroGPSError {
217
340
  name: string;
341
+ retryAfterSeconds?: number;
218
342
  }
219
343
  declare class ServerError extends CatastroGPSError {
220
344
  name: string;
@@ -226,4 +350,4 @@ declare class NetworkError extends CatastroGPSError {
226
350
  name: string;
227
351
  }
228
352
 
229
- export { type AddressMatch, type AgricultureData, AmbiguousReferenceError, AuthenticationError, COUNTRY_CODES, CatastroGPS, CatastroGPSError, type CatastroGPSOptions, type CountryCode, type CountryOption, CoverageError, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, ENRICHMENT_COUNTRY_CODES, type EnrichmentCountryCode, type ExportFormat, type FetchLike, ForbiddenError, NetworkError, NotFoundError, type Parcel, type ParcelGeometry, type PointMatch, type PointQuery, type Quota, QuotaExceededError, RateLimitError, type ResolveCandidate, type ResolveResult, ServerError, ServiceUnavailableError, type SolarEstimate, TimeoutError, VERSION, ValidationError };
353
+ export { type AddressCandidate, type AddressCandidatesQuery, type AddressCandidatesResult, type AddressMatch, type AgricultureData, AmbiguousReferenceError, AuthenticationError, COUNTRY_CODES, CatastroGPS, CatastroGPSError, type CatastroGPSOptions, type ConditionalResponse, type Construction, type CountryCode, type CountryOption, CoverageError, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, ENRICHMENT_COUNTRY_CODES, type EnrichmentCountryCode, type ExportFormat, type FetchLike, ForbiddenError, type GetUnitsOptions, NetworkError, NotFoundError, type Parcel, type ParcelGeometry, type PointMatch, type PointQuery, type Quota, QuotaExceededError, RateLimitError, type ResolveCandidate, type ResolveResult, ServerError, ServiceUnavailableError, type SolarEstimate, TimeoutError, type Unit, type UnitsPage, type UnitsPageData, type UnitsResult, VERSION, ValidationError };
package/dist/index.d.ts CHANGED
@@ -117,6 +117,110 @@ interface ResolveResult {
117
117
  candidates: ResolveCandidate[];
118
118
  ambiguous: boolean;
119
119
  }
120
+ interface AddressCandidatesQuery {
121
+ q?: string;
122
+ street?: string;
123
+ number?: number;
124
+ municipality?: string;
125
+ postcode?: string;
126
+ limit?: number;
127
+ }
128
+ interface AddressCandidate {
129
+ refCatastral: string;
130
+ pais: 'ES' | 'PV' | 'NA' | string;
131
+ direccion: string;
132
+ numero?: number;
133
+ codigoPostal?: string;
134
+ municipio: string;
135
+ provincia: string;
136
+ latitud: number;
137
+ longitud: number;
138
+ confianza: number;
139
+ coincideNumero: boolean;
140
+ coincideMunicipio: boolean;
141
+ enCopia: boolean;
142
+ direccionCatastro?: string;
143
+ uso?: string;
144
+ viviendas?: number;
145
+ anioConstruccion?: number;
146
+ }
147
+ interface AddressCandidatesResult {
148
+ consulta: {
149
+ texto: string;
150
+ calle?: string;
151
+ numero?: number;
152
+ municipio?: string;
153
+ codigoPostal?: string;
154
+ };
155
+ candidatos: AddressCandidate[];
156
+ attribution: string;
157
+ }
158
+ interface Unit {
159
+ refCatastral: string;
160
+ escalera: string;
161
+ planta: string;
162
+ puerta: string;
163
+ uso: string;
164
+ superficie: number;
165
+ descripcion: string;
166
+ participacion?: number;
167
+ anio?: number;
168
+ direccion?: string;
169
+ }
170
+ interface Construction {
171
+ escalera: string;
172
+ planta: string;
173
+ puerta: string;
174
+ uso: string;
175
+ superficie: number;
176
+ descripcion: string;
177
+ }
178
+ interface UnitsPageData {
179
+ refCatastral: string;
180
+ direccion?: string;
181
+ codigoPostal?: string;
182
+ municipio?: string;
183
+ provincia?: string;
184
+ usoGeneral?: string;
185
+ superficieTotal?: number;
186
+ anioConstruccion?: number;
187
+ participacion?: string;
188
+ totalUnidades: number;
189
+ totalUnidadesFinca: number;
190
+ unidades: Unit[];
191
+ construcciones?: Construction[];
192
+ truncated: boolean;
193
+ nextCursor?: string;
194
+ dataSource: 'clone' | 'catastro' | string;
195
+ dataDate: string;
196
+ attribution: string;
197
+ }
198
+ interface UnitsPage {
199
+ cursor?: string;
200
+ etag?: string;
201
+ notModified: boolean;
202
+ data: UnitsPageData;
203
+ }
204
+ interface UnitsResult {
205
+ refCatastral: string;
206
+ direccion?: string;
207
+ codigoPostal?: string;
208
+ municipio?: string;
209
+ provincia?: string;
210
+ usoGeneral?: string;
211
+ anioConstruccion?: number;
212
+ totalUnidadesFinca: number;
213
+ unidades: Unit[];
214
+ construcciones: Construction[];
215
+ dataSource: string;
216
+ dataDate: string;
217
+ attribution: string;
218
+ changed: boolean;
219
+ pages: UnitsPage[];
220
+ }
221
+ interface GetUnitsOptions {
222
+ previous?: UnitsResult;
223
+ }
120
224
 
121
225
  declare class ExportResource {
122
226
  private readonly client;
@@ -124,6 +228,14 @@ declare class ExportResource {
124
228
  file(reference: string, format: ExportFormat, options?: CountryOption): Promise<ArrayBuffer>;
125
229
  }
126
230
 
231
+ declare class FincasResource {
232
+ private readonly client;
233
+ constructor(client: CatastroGPS);
234
+ searchAddress(query: string | AddressCandidatesQuery): Promise<AddressCandidatesResult>;
235
+ pages(refcat14: string, options?: GetUnitsOptions): AsyncGenerator<UnitsPage>;
236
+ units(refcat14: string, options?: GetUnitsOptions): Promise<UnitsResult>;
237
+ }
238
+
127
239
  interface PointQuery extends CountryOption {
128
240
  lat: number;
129
241
  lng: number;
@@ -147,10 +259,17 @@ interface RequestOptions {
147
259
  query?: Query;
148
260
  body?: unknown;
149
261
  raw?: boolean;
262
+ ifNoneMatch?: string;
263
+ }
264
+ interface ConditionalResponse<T> {
265
+ data?: T;
266
+ etag?: string;
267
+ notModified: boolean;
150
268
  }
151
269
  declare class CatastroGPS {
152
270
  readonly parcels: ParcelsResource;
153
271
  readonly export: ExportResource;
272
+ readonly fincas: FincasResource;
154
273
  lastQuota?: Quota;
155
274
  private readonly apiKey;
156
275
  private readonly baseUrl;
@@ -158,10 +277,14 @@ declare class CatastroGPS {
158
277
  private readonly maxRetries;
159
278
  private readonly fetchImpl;
160
279
  constructor(options?: CatastroGPSOptions);
280
+ searchAddressCandidates(query: string | AddressCandidatesQuery): Promise<AddressCandidatesResult>;
281
+ getUnits(refcat14: string, options?: GetUnitsOptions): Promise<UnitsResult>;
282
+ iterUnitPages(refcat14: string, options?: GetUnitsOptions): AsyncGenerator<UnitsPage>;
161
283
  resolve(text: string, options?: {
162
284
  hint?: string;
163
285
  }): Promise<ResolveResult>;
164
286
  request<T>(method: 'GET' | 'POST', path: string, options?: RequestOptions): Promise<T>;
287
+ conditionalRequest<T>(method: 'GET' | 'POST', path: string, options?: RequestOptions): Promise<ConditionalResponse<T>>;
165
288
  private once;
166
289
  private buildUrl;
167
290
  private shouldRetry;
@@ -169,7 +292,7 @@ declare class CatastroGPS {
169
292
  private captureQuota;
170
293
  }
171
294
 
172
- declare const VERSION = "1.0.0";
295
+ declare const VERSION = "1.1.0";
173
296
 
174
297
  declare class CatastroGPSError extends Error {
175
298
  readonly status?: number;
@@ -215,6 +338,7 @@ declare class RateLimitError extends CatastroGPSError {
215
338
  }
216
339
  declare class ServiceUnavailableError extends CatastroGPSError {
217
340
  name: string;
341
+ retryAfterSeconds?: number;
218
342
  }
219
343
  declare class ServerError extends CatastroGPSError {
220
344
  name: string;
@@ -226,4 +350,4 @@ declare class NetworkError extends CatastroGPSError {
226
350
  name: string;
227
351
  }
228
352
 
229
- export { type AddressMatch, type AgricultureData, AmbiguousReferenceError, AuthenticationError, COUNTRY_CODES, CatastroGPS, CatastroGPSError, type CatastroGPSOptions, type CountryCode, type CountryOption, CoverageError, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, ENRICHMENT_COUNTRY_CODES, type EnrichmentCountryCode, type ExportFormat, type FetchLike, ForbiddenError, NetworkError, NotFoundError, type Parcel, type ParcelGeometry, type PointMatch, type PointQuery, type Quota, QuotaExceededError, RateLimitError, type ResolveCandidate, type ResolveResult, ServerError, ServiceUnavailableError, type SolarEstimate, TimeoutError, VERSION, ValidationError };
353
+ export { type AddressCandidate, type AddressCandidatesQuery, type AddressCandidatesResult, type AddressMatch, type AgricultureData, AmbiguousReferenceError, AuthenticationError, COUNTRY_CODES, CatastroGPS, CatastroGPSError, type CatastroGPSOptions, type ConditionalResponse, type Construction, type CountryCode, type CountryOption, CoverageError, DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, ENRICHMENT_COUNTRY_CODES, type EnrichmentCountryCode, type ExportFormat, type FetchLike, ForbiddenError, type GetUnitsOptions, NetworkError, NotFoundError, type Parcel, type ParcelGeometry, type PointMatch, type PointQuery, type Quota, QuotaExceededError, RateLimitError, type ResolveCandidate, type ResolveResult, ServerError, ServiceUnavailableError, type SolarEstimate, TimeoutError, type Unit, type UnitsPage, type UnitsPageData, type UnitsResult, VERSION, ValidationError };
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ var RateLimitError = class extends CatastroGPSError {
89
89
  };
90
90
  var ServiceUnavailableError = class extends CatastroGPSError {
91
91
  name = "ServiceUnavailableError";
92
+ retryAfterSeconds;
92
93
  };
93
94
  var ServerError = class extends CatastroGPSError {
94
95
  name = "ServerError";
@@ -116,7 +117,11 @@ function errorFromResponse(status, body, retryAfter) {
116
117
  if (status === 403) return new ForbiddenError(message, options);
117
118
  if (status === 404) return new NotFoundError(message, options);
118
119
  if (status === 400 || status === 422) return new ValidationError(message, options);
119
- if (status === 503) return new ServiceUnavailableError(message, options);
120
+ if (status === 503) {
121
+ const error = new ServiceUnavailableError(message, options);
122
+ error.retryAfterSeconds = retryAfter;
123
+ return error;
124
+ }
120
125
  if (status >= 500) return new ServerError(message, options);
121
126
  return new CatastroGPSError(message, options);
122
127
  }
@@ -179,8 +184,73 @@ function parcelPath(reference, suffix = "") {
179
184
  return `/api/catastro/${encodeURIComponent(reference.trim())}${suffix}`;
180
185
  }
181
186
 
187
+ // src/resources/fincas.ts
188
+ var MAX_PAGES = 1e3;
189
+ var FincasResource = class {
190
+ constructor(client) {
191
+ this.client = client;
192
+ }
193
+ client;
194
+ async searchAddress(query) {
195
+ const params = typeof query === "string" ? { q: query } : query;
196
+ if (!params.q?.trim() && !params.street?.trim()) throw new CatastroGPSError("q or street is required");
197
+ return this.client.request("GET", "/api/search/address/candidates", {
198
+ query: {
199
+ q: params.q?.trim(),
200
+ street: params.street?.trim(),
201
+ number: params.number,
202
+ municipality: params.municipality?.trim(),
203
+ postcode: params.postcode?.trim(),
204
+ limit: params.limit
205
+ }
206
+ });
207
+ }
208
+ async *pages(refcat14, options = {}) {
209
+ const path = parcelPath(refcat14, "/units");
210
+ const previousByCursor = new Map((options.previous?.pages ?? []).map((page) => [page.cursor ?? "", page]));
211
+ let cursor;
212
+ for (let count = 0; count < MAX_PAGES; count += 1) {
213
+ const previous = previousByCursor.get(cursor ?? "");
214
+ const response = await this.client.conditionalRequest("GET", path, {
215
+ query: { cursor },
216
+ ifNoneMatch: previous?.etag
217
+ });
218
+ const data = response.notModified && previous ? previous.data : response.data;
219
+ if (!data) throw new CatastroGPSError("The API answered 304 for a page that was not in the previous import");
220
+ const page = { cursor, etag: response.etag, notModified: response.notModified, data };
221
+ yield page;
222
+ if (!data.truncated || !data.nextCursor) return;
223
+ cursor = data.nextCursor;
224
+ }
225
+ throw new CatastroGPSError(`Stopped after ${MAX_PAGES} pages`);
226
+ }
227
+ async units(refcat14, options = {}) {
228
+ const pages = [];
229
+ for await (const page of this.pages(refcat14, options)) pages.push(page);
230
+ const first = pages[0].data;
231
+ const last = pages[pages.length - 1].data;
232
+ return {
233
+ refCatastral: first.refCatastral,
234
+ direccion: first.direccion,
235
+ codigoPostal: first.codigoPostal,
236
+ municipio: first.municipio,
237
+ provincia: first.provincia,
238
+ usoGeneral: first.usoGeneral,
239
+ anioConstruccion: first.anioConstruccion,
240
+ totalUnidadesFinca: last.totalUnidadesFinca ?? pages.reduce((sum, p) => sum + p.data.unidades.length, 0),
241
+ unidades: pages.flatMap((page) => page.data.unidades),
242
+ construcciones: pages.flatMap((page) => page.data.construcciones ?? []),
243
+ dataSource: first.dataSource,
244
+ dataDate: first.dataDate,
245
+ attribution: first.attribution,
246
+ changed: !pages.every((page) => page.notModified) || pages.length !== (options.previous?.pages.length ?? -1),
247
+ pages
248
+ };
249
+ }
250
+ };
251
+
182
252
  // src/version.ts
183
- var VERSION = "1.0.0";
253
+ var VERSION = "1.1.0";
184
254
 
185
255
  // src/client.ts
186
256
  var DEFAULT_BASE_URL = "https://api.catastrogps.es";
@@ -188,9 +258,12 @@ var DEFAULT_TIMEOUT_MS = 3e4;
188
258
  var DEFAULT_MAX_RETRIES = 2;
189
259
  var BACKOFF_BASE_MS = 500;
190
260
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([502, 503, 504]);
261
+ var NOT_MODIFIED = 304;
262
+ var MAX_RETRY_AFTER_SECONDS = 60;
191
263
  var CatastroGPS = class {
192
264
  parcels;
193
265
  export;
266
+ fincas;
194
267
  lastQuota;
195
268
  apiKey;
196
269
  baseUrl;
@@ -215,11 +288,25 @@ var CatastroGPS = class {
215
288
  this.fetchImpl = fetchImpl;
216
289
  this.parcels = new ParcelsResource(this);
217
290
  this.export = new ExportResource(this);
291
+ this.fincas = new FincasResource(this);
292
+ }
293
+ async searchAddressCandidates(query) {
294
+ return this.fincas.searchAddress(query);
295
+ }
296
+ async getUnits(refcat14, options = {}) {
297
+ return this.fincas.units(refcat14, options);
298
+ }
299
+ iterUnitPages(refcat14, options = {}) {
300
+ return this.fincas.pages(refcat14, options);
218
301
  }
219
302
  async resolve(text, options = {}) {
220
303
  return this.request("GET", "/api/resolve", { query: { q: text, hint: options.hint } });
221
304
  }
222
305
  async request(method, path, options = {}) {
306
+ const result = await this.conditionalRequest(method, path, options);
307
+ return result.data;
308
+ }
309
+ async conditionalRequest(method, path, options = {}) {
223
310
  const url = this.buildUrl(path, options.query);
224
311
  let attempt = 0;
225
312
  for (; ; ) {
@@ -241,6 +328,7 @@ var CatastroGPS = class {
241
328
  "User-Agent": `catastrogps-js/${VERSION}`
242
329
  };
243
330
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
331
+ if (options.ifNoneMatch) headers["If-None-Match"] = options.ifNoneMatch;
244
332
  let response;
245
333
  try {
246
334
  response = await this.fetchImpl(url, {
@@ -258,14 +346,16 @@ var CatastroGPS = class {
258
346
  clearTimeout(timer);
259
347
  }
260
348
  this.captureQuota(response.headers);
349
+ const etag = response.headers.get("ETag") ?? void 0;
350
+ if (response.status === NOT_MODIFIED) return { etag: etag ?? options.ifNoneMatch, notModified: true };
261
351
  if (!response.ok) {
262
352
  const body2 = await readJson(response);
263
353
  const retryAfter = Number(response.headers.get("Retry-After"));
264
354
  throw errorFromResponse(response.status, body2, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0);
265
355
  }
266
- if (options.raw) return await response.arrayBuffer();
356
+ if (options.raw) return { data: await response.arrayBuffer(), etag, notModified: false };
267
357
  const body = await readJson(response);
268
- return "data" in body ? body.data : body;
358
+ return { data: "data" in body ? body.data : body, etag, notModified: false };
269
359
  }
270
360
  buildUrl(path, query) {
271
361
  const url = new URL(`${this.baseUrl}${path}`);
@@ -281,21 +371,29 @@ var CatastroGPS = class {
281
371
  return error instanceof CatastroGPSError && error.status !== void 0 && RETRYABLE_STATUS.has(error.status);
282
372
  }
283
373
  retryDelayMs(error, attempt) {
284
- if (error instanceof RateLimitError && error.retryAfterSeconds) return error.retryAfterSeconds * 1e3;
374
+ if ((error instanceof RateLimitError || error instanceof ServiceUnavailableError) && error.retryAfterSeconds) {
375
+ return Math.min(error.retryAfterSeconds, MAX_RETRY_AFTER_SECONDS) * 1e3;
376
+ }
285
377
  return BACKOFF_BASE_MS * 2 ** attempt;
286
378
  }
287
379
  captureQuota(headers) {
288
- const limit = headers.get("X-RateLimit-Limit");
289
- if (limit === null) return;
290
- const remaining = headers.get("X-RateLimit-Remaining");
291
- this.lastQuota = {
292
- plan: headers.get("X-Quota-Tier") ?? void 0,
293
- limit: Number(limit),
294
- remaining: remaining === null ? void 0 : Number(remaining),
295
- resetsAt: headers.get("X-RateLimit-Reset") ?? void 0
296
- };
380
+ const quota = readQuota(headers);
381
+ if (quota) this.lastQuota = quota;
297
382
  }
298
383
  };
384
+ function readQuota(headers) {
385
+ const plan = headers.get("X-Quota-Tier") ?? void 0;
386
+ const family = headers.has("X-Quota-Limit") ? "X-Quota" : plan ? "X-RateLimit" : void 0;
387
+ if (!family) return void 0;
388
+ const limit = headers.get(`${family}-Limit`);
389
+ const remaining = headers.get(`${family}-Remaining`);
390
+ return {
391
+ plan,
392
+ limit: limit === null ? void 0 : Number(limit),
393
+ remaining: remaining === null ? void 0 : Number(remaining),
394
+ resetsAt: headers.get(`${family}-Reset`) ?? void 0
395
+ };
396
+ }
299
397
  async function readJson(response) {
300
398
  try {
301
399
  const parsed = await response.json();
package/dist/index.mjs CHANGED
@@ -44,6 +44,7 @@ var RateLimitError = class extends CatastroGPSError {
44
44
  };
45
45
  var ServiceUnavailableError = class extends CatastroGPSError {
46
46
  name = "ServiceUnavailableError";
47
+ retryAfterSeconds;
47
48
  };
48
49
  var ServerError = class extends CatastroGPSError {
49
50
  name = "ServerError";
@@ -71,7 +72,11 @@ function errorFromResponse(status, body, retryAfter) {
71
72
  if (status === 403) return new ForbiddenError(message, options);
72
73
  if (status === 404) return new NotFoundError(message, options);
73
74
  if (status === 400 || status === 422) return new ValidationError(message, options);
74
- if (status === 503) return new ServiceUnavailableError(message, options);
75
+ if (status === 503) {
76
+ const error = new ServiceUnavailableError(message, options);
77
+ error.retryAfterSeconds = retryAfter;
78
+ return error;
79
+ }
75
80
  if (status >= 500) return new ServerError(message, options);
76
81
  return new CatastroGPSError(message, options);
77
82
  }
@@ -134,8 +139,73 @@ function parcelPath(reference, suffix = "") {
134
139
  return `/api/catastro/${encodeURIComponent(reference.trim())}${suffix}`;
135
140
  }
136
141
 
142
+ // src/resources/fincas.ts
143
+ var MAX_PAGES = 1e3;
144
+ var FincasResource = class {
145
+ constructor(client) {
146
+ this.client = client;
147
+ }
148
+ client;
149
+ async searchAddress(query) {
150
+ const params = typeof query === "string" ? { q: query } : query;
151
+ if (!params.q?.trim() && !params.street?.trim()) throw new CatastroGPSError("q or street is required");
152
+ return this.client.request("GET", "/api/search/address/candidates", {
153
+ query: {
154
+ q: params.q?.trim(),
155
+ street: params.street?.trim(),
156
+ number: params.number,
157
+ municipality: params.municipality?.trim(),
158
+ postcode: params.postcode?.trim(),
159
+ limit: params.limit
160
+ }
161
+ });
162
+ }
163
+ async *pages(refcat14, options = {}) {
164
+ const path = parcelPath(refcat14, "/units");
165
+ const previousByCursor = new Map((options.previous?.pages ?? []).map((page) => [page.cursor ?? "", page]));
166
+ let cursor;
167
+ for (let count = 0; count < MAX_PAGES; count += 1) {
168
+ const previous = previousByCursor.get(cursor ?? "");
169
+ const response = await this.client.conditionalRequest("GET", path, {
170
+ query: { cursor },
171
+ ifNoneMatch: previous?.etag
172
+ });
173
+ const data = response.notModified && previous ? previous.data : response.data;
174
+ if (!data) throw new CatastroGPSError("The API answered 304 for a page that was not in the previous import");
175
+ const page = { cursor, etag: response.etag, notModified: response.notModified, data };
176
+ yield page;
177
+ if (!data.truncated || !data.nextCursor) return;
178
+ cursor = data.nextCursor;
179
+ }
180
+ throw new CatastroGPSError(`Stopped after ${MAX_PAGES} pages`);
181
+ }
182
+ async units(refcat14, options = {}) {
183
+ const pages = [];
184
+ for await (const page of this.pages(refcat14, options)) pages.push(page);
185
+ const first = pages[0].data;
186
+ const last = pages[pages.length - 1].data;
187
+ return {
188
+ refCatastral: first.refCatastral,
189
+ direccion: first.direccion,
190
+ codigoPostal: first.codigoPostal,
191
+ municipio: first.municipio,
192
+ provincia: first.provincia,
193
+ usoGeneral: first.usoGeneral,
194
+ anioConstruccion: first.anioConstruccion,
195
+ totalUnidadesFinca: last.totalUnidadesFinca ?? pages.reduce((sum, p) => sum + p.data.unidades.length, 0),
196
+ unidades: pages.flatMap((page) => page.data.unidades),
197
+ construcciones: pages.flatMap((page) => page.data.construcciones ?? []),
198
+ dataSource: first.dataSource,
199
+ dataDate: first.dataDate,
200
+ attribution: first.attribution,
201
+ changed: !pages.every((page) => page.notModified) || pages.length !== (options.previous?.pages.length ?? -1),
202
+ pages
203
+ };
204
+ }
205
+ };
206
+
137
207
  // src/version.ts
138
- var VERSION = "1.0.0";
208
+ var VERSION = "1.1.0";
139
209
 
140
210
  // src/client.ts
141
211
  var DEFAULT_BASE_URL = "https://api.catastrogps.es";
@@ -143,9 +213,12 @@ var DEFAULT_TIMEOUT_MS = 3e4;
143
213
  var DEFAULT_MAX_RETRIES = 2;
144
214
  var BACKOFF_BASE_MS = 500;
145
215
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([502, 503, 504]);
216
+ var NOT_MODIFIED = 304;
217
+ var MAX_RETRY_AFTER_SECONDS = 60;
146
218
  var CatastroGPS = class {
147
219
  parcels;
148
220
  export;
221
+ fincas;
149
222
  lastQuota;
150
223
  apiKey;
151
224
  baseUrl;
@@ -170,11 +243,25 @@ var CatastroGPS = class {
170
243
  this.fetchImpl = fetchImpl;
171
244
  this.parcels = new ParcelsResource(this);
172
245
  this.export = new ExportResource(this);
246
+ this.fincas = new FincasResource(this);
247
+ }
248
+ async searchAddressCandidates(query) {
249
+ return this.fincas.searchAddress(query);
250
+ }
251
+ async getUnits(refcat14, options = {}) {
252
+ return this.fincas.units(refcat14, options);
253
+ }
254
+ iterUnitPages(refcat14, options = {}) {
255
+ return this.fincas.pages(refcat14, options);
173
256
  }
174
257
  async resolve(text, options = {}) {
175
258
  return this.request("GET", "/api/resolve", { query: { q: text, hint: options.hint } });
176
259
  }
177
260
  async request(method, path, options = {}) {
261
+ const result = await this.conditionalRequest(method, path, options);
262
+ return result.data;
263
+ }
264
+ async conditionalRequest(method, path, options = {}) {
178
265
  const url = this.buildUrl(path, options.query);
179
266
  let attempt = 0;
180
267
  for (; ; ) {
@@ -196,6 +283,7 @@ var CatastroGPS = class {
196
283
  "User-Agent": `catastrogps-js/${VERSION}`
197
284
  };
198
285
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
286
+ if (options.ifNoneMatch) headers["If-None-Match"] = options.ifNoneMatch;
199
287
  let response;
200
288
  try {
201
289
  response = await this.fetchImpl(url, {
@@ -213,14 +301,16 @@ var CatastroGPS = class {
213
301
  clearTimeout(timer);
214
302
  }
215
303
  this.captureQuota(response.headers);
304
+ const etag = response.headers.get("ETag") ?? void 0;
305
+ if (response.status === NOT_MODIFIED) return { etag: etag ?? options.ifNoneMatch, notModified: true };
216
306
  if (!response.ok) {
217
307
  const body2 = await readJson(response);
218
308
  const retryAfter = Number(response.headers.get("Retry-After"));
219
309
  throw errorFromResponse(response.status, body2, Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0);
220
310
  }
221
- if (options.raw) return await response.arrayBuffer();
311
+ if (options.raw) return { data: await response.arrayBuffer(), etag, notModified: false };
222
312
  const body = await readJson(response);
223
- return "data" in body ? body.data : body;
313
+ return { data: "data" in body ? body.data : body, etag, notModified: false };
224
314
  }
225
315
  buildUrl(path, query) {
226
316
  const url = new URL(`${this.baseUrl}${path}`);
@@ -236,21 +326,29 @@ var CatastroGPS = class {
236
326
  return error instanceof CatastroGPSError && error.status !== void 0 && RETRYABLE_STATUS.has(error.status);
237
327
  }
238
328
  retryDelayMs(error, attempt) {
239
- if (error instanceof RateLimitError && error.retryAfterSeconds) return error.retryAfterSeconds * 1e3;
329
+ if ((error instanceof RateLimitError || error instanceof ServiceUnavailableError) && error.retryAfterSeconds) {
330
+ return Math.min(error.retryAfterSeconds, MAX_RETRY_AFTER_SECONDS) * 1e3;
331
+ }
240
332
  return BACKOFF_BASE_MS * 2 ** attempt;
241
333
  }
242
334
  captureQuota(headers) {
243
- const limit = headers.get("X-RateLimit-Limit");
244
- if (limit === null) return;
245
- const remaining = headers.get("X-RateLimit-Remaining");
246
- this.lastQuota = {
247
- plan: headers.get("X-Quota-Tier") ?? void 0,
248
- limit: Number(limit),
249
- remaining: remaining === null ? void 0 : Number(remaining),
250
- resetsAt: headers.get("X-RateLimit-Reset") ?? void 0
251
- };
335
+ const quota = readQuota(headers);
336
+ if (quota) this.lastQuota = quota;
252
337
  }
253
338
  };
339
+ 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
+ };
351
+ }
254
352
  async function readJson(response) {
255
353
  try {
256
354
  const parsed = await response.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catastrogps",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
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",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",