@fin.cx/opendata 3.6.0 → 4.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.
Files changed (93) hide show
  1. package/.smartconfig.json +23 -20
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +64 -0
  4. package/dist_ts/handelsregister/classes.handelsregisterclient.js +397 -0
  5. package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +56 -0
  6. package/dist_ts/handelsregister/classes.handelsregistererror.js +37 -0
  7. package/dist_ts/handelsregister/classes.portalsession.d.ts +46 -0
  8. package/dist_ts/handelsregister/classes.portalsession.js +160 -0
  9. package/dist_ts/handelsregister/classes.slidingwindowgate.d.ts +32 -0
  10. package/dist_ts/handelsregister/classes.slidingwindowgate.js +55 -0
  11. package/dist_ts/handelsregister/constants.d.ts +13 -0
  12. package/dist_ts/handelsregister/constants.js +19 -0
  13. package/dist_ts/handelsregister/data.codelists.d.ts +29 -0
  14. package/dist_ts/handelsregister/data.codelists.js +616 -0
  15. package/dist_ts/handelsregister/forms.d.ts +27 -0
  16. package/dist_ts/handelsregister/forms.js +77 -0
  17. package/dist_ts/handelsregister/html.d.ts +26 -0
  18. package/dist_ts/handelsregister/html.js +82 -0
  19. package/dist_ts/handelsregister/index.d.ts +9 -0
  20. package/dist_ts/handelsregister/index.js +10 -0
  21. package/dist_ts/handelsregister/interfaces.d.ts +240 -0
  22. package/dist_ts/handelsregister/interfaces.js +2 -0
  23. package/dist_ts/handelsregister/parse.searchresults.d.ts +35 -0
  24. package/dist_ts/handelsregister/parse.searchresults.js +175 -0
  25. package/dist_ts/handelsregister/parse.structuredcontent.d.ts +6 -0
  26. package/dist_ts/handelsregister/parse.structuredcontent.js +306 -0
  27. package/dist_ts/handelsregister/plugins.d.ts +4 -0
  28. package/dist_ts/handelsregister/plugins.js +8 -0
  29. package/dist_ts/handelsregister/references.d.ts +38 -0
  30. package/dist_ts/handelsregister/references.js +146 -0
  31. package/dist_ts/holidays/calendar.d.ts +32 -0
  32. package/dist_ts/holidays/calendar.js +200 -0
  33. package/dist_ts/holidays/classes.holidaycalendarerror.d.ts +14 -0
  34. package/dist_ts/holidays/classes.holidaycalendarerror.js +10 -0
  35. package/dist_ts/holidays/data.holidays.d.ts +46 -0
  36. package/dist_ts/holidays/data.holidays.js +124 -0
  37. package/dist_ts/holidays/index.d.ts +4 -0
  38. package/dist_ts/holidays/index.js +7 -0
  39. package/dist_ts/holidays/interfaces.d.ts +42 -0
  40. package/dist_ts/holidays/interfaces.js +2 -0
  41. package/dist_ts/index.d.ts +0 -2
  42. package/dist_ts/index.js +1 -3
  43. package/dist_ts/plugins.d.ts +2 -20
  44. package/dist_ts/plugins.js +3 -22
  45. package/package.json +40 -37
  46. package/readme.md +250 -119
  47. package/ts/00_commitinfo_data.ts +2 -2
  48. package/ts/handelsregister/classes.handelsregisterclient.ts +474 -0
  49. package/ts/handelsregister/classes.handelsregistererror.ts +82 -0
  50. package/ts/handelsregister/classes.portalsession.ts +191 -0
  51. package/ts/handelsregister/classes.slidingwindowgate.ts +74 -0
  52. package/ts/handelsregister/constants.ts +30 -0
  53. package/ts/handelsregister/data.codelists.ts +640 -0
  54. package/ts/handelsregister/forms.ts +99 -0
  55. package/ts/handelsregister/html.ts +113 -0
  56. package/ts/handelsregister/index.ts +27 -0
  57. package/ts/handelsregister/interfaces.ts +272 -0
  58. package/ts/handelsregister/parse.searchresults.ts +241 -0
  59. package/ts/handelsregister/parse.structuredcontent.ts +338 -0
  60. package/ts/handelsregister/plugins.ts +15 -0
  61. package/ts/handelsregister/references.ts +189 -0
  62. package/ts/holidays/calendar.ts +261 -0
  63. package/ts/holidays/classes.holidaycalendarerror.ts +20 -0
  64. package/ts/holidays/data.holidays.ts +177 -0
  65. package/ts/holidays/index.ts +18 -0
  66. package/ts/holidays/interfaces.ts +87 -0
  67. package/ts/index.ts +0 -2
  68. package/ts/plugins.ts +0 -39
  69. package/dist_ts/classes.businessrecord.d.ts +0 -38
  70. package/dist_ts/classes.businessrecord.js +0 -90
  71. package/dist_ts/classes.handelsregister.d.ts +0 -134
  72. package/dist_ts/classes.handelsregister.js +0 -307
  73. package/dist_ts/classes.jsonldata.d.ts +0 -43
  74. package/dist_ts/classes.jsonldata.js +0 -68
  75. package/dist_ts/classes.main.opendata.d.ts +0 -25
  76. package/dist_ts/classes.main.opendata.js +0 -91
  77. package/dist_ts/laws/classes.lawrecord.d.ts +0 -24
  78. package/dist_ts/laws/classes.lawrecord.js +0 -181
  79. package/dist_ts/laws/classes.lawservice.d.ts +0 -52
  80. package/dist_ts/laws/classes.lawservice.js +0 -740
  81. package/dist_ts/laws/index.d.ts +0 -3
  82. package/dist_ts/laws/index.js +0 -4
  83. package/dist_ts/laws/interfaces.law.d.ts +0 -35
  84. package/dist_ts/laws/interfaces.law.js +0 -2
  85. package/readme.hints.md +0 -89
  86. package/ts/classes.businessrecord.ts +0 -67
  87. package/ts/classes.handelsregister.ts +0 -385
  88. package/ts/classes.jsonldata.ts +0 -114
  89. package/ts/classes.main.opendata.ts +0 -120
  90. package/ts/laws/classes.lawrecord.ts +0 -81
  91. package/ts/laws/classes.lawservice.ts +0 -1010
  92. package/ts/laws/index.ts +0 -3
  93. package/ts/laws/interfaces.law.ts +0 -48
package/readme.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @fin.cx/opendata
2
2
 
3
- `@fin.cx/opendata` is a TypeScript toolbox for serious data plumbing: market data, SEC fundamentals, German company registry workflows, and locally synced law corpora. It is built for programmers who want scriptable APIs, local persistence, and real workflows instead of a thin wrapper around one remote endpoint.
3
+ `@fin.cx/opendata` is a TypeScript toolbox for open business and market data: stock and crypto prices, SEC fundamentals, lookups in the German registers through the register portal of the federal states, and the statutory public holidays of the German federal states. It is built for programmers who want scriptable APIs and real workflows instead of a thin wrapper around one remote endpoint.
4
4
 
5
- It currently gives you four practical lanes in one package:
5
+ It gives you four practical lanes in one package:
6
6
 
7
7
  - `StockPriceService` for stock and crypto prices with caching, retries, failover, historical ranges, and intraday support
8
8
  - `FundamentalsService` and `StockDataService` for SEC fundamentals and combined price + fundamentals payloads
9
- - `OpenData` for German company ingestion plus browser-driven Handelsregister lookup and document download
10
- - `LawService` for syncing and searching German, EU, and US law texts in a local embedded database
9
+ - `HandelsregisterClient`, from `@fin.cx/opendata/handelsregister`, for single lookups in the commercial, cooperative, partnership, association and civil-law partnership registers over plain HTTP, without any storage
10
+ - the holiday calendar, from `@fin.cx/opendata/holidays`, for the statutory public holidays of each German federal state and the working days of § 193 BGB, computed per year without data files or requests
11
11
 
12
12
  ## Issue Reporting and Security
13
13
 
@@ -36,17 +36,28 @@ pnpm add @fin.cx/opendata
36
36
  - `SecEdgarProvider`
37
37
  Fundamentals provider backed by SEC EDGAR company facts.
38
38
 
39
- ### German Company Data
39
+ ### German Registers
40
40
 
41
- - `OpenData`
42
- Starts a local embedded database, imports the public German company JSONL dump, and exposes a `handelsregister` helper for company lookup and file download.
41
+ Everything here comes from `@fin.cx/opendata/handelsregister`. That entry point imports nothing else of the package, needs no database, no browser and no download folder, and keeps nothing between calls.
43
42
 
44
- ### Laws
43
+ - `HandelsregisterClient`
44
+ Searches the register portal (handelsregister.de) by name or register number and retrieves one register sheet with its structured register content (SI) parsed into a typed result.
45
+ - `SlidingWindowGate`
46
+ Holds the portal's limit of 60 searches and retrievals per hour. The client asks it before every counted request.
47
+ - `parseStructuredContent()` and `parseSearchResults()`
48
+ The parsers on their own, for an SI file or a result page someone saved from the portal. They make no request.
49
+ - `registerCourts`, `findRegisterCourt()`, `parseRegisterReferenceText()`, `formatRegisterReference()`
50
+ The 150 register courts with their XJustiz codes, and helpers for register references as businesses print them (`Amtsgericht Bremen HRB 35230 HB`).
45
51
 
46
- - `LawService`
47
- Syncs laws into a local embedded database and lets you search them later.
48
- - `LawRecord`
49
- The stored law document model returned by sync and search operations.
52
+ ### German Public Holidays
53
+
54
+ Everything here comes from `@fin.cx/opendata/holidays`. That entry point imports nothing else of the package, has no dependencies, and makes no request.
55
+
56
+ - `getPublicHolidays(year, state)`, `getPublicHolidaysOn(date, state)`
57
+ The statutory public holidays of a federal state in a year or on a date, each with its legal basis.
58
+ - `isPublicHoliday()`, `isWorkingDay()`, `nextWorkingDay()`, `workingDayOnOrAfter()`
59
+ Holidays and working days in the sense of § 193 BGB: neither a Saturday, nor a Sunday, nor a statutory public holiday.
60
+ - `easterSunday(year)`, `germanStates`, `findGermanState()`
50
61
 
51
62
  ## Before You Start
52
63
 
@@ -54,10 +65,8 @@ pnpm add @fin.cx/opendata
54
65
  - `MarketstackProvider` requires an API key.
55
66
  - `SecEdgarProvider` requires a valid `User-Agent` string in the format `Company Name email@example.com`.
56
67
  - `CoinGeckoProvider` works without an API key, but a key gives you better rate limits.
57
- - `OpenData` requires `nogitDir`, `downloadDir`, and `germanBusinessDataDir`.
58
- - `OpenData.start()` is required before using `OpenData.handelsregister` or `buildInitialDb()`.
59
- - `LawService` stores data locally. It auto-starts on demand, but explicit `start()` and `stop()` keep lifecycle handling cleaner.
60
- - EU law syncing and Handelsregister access use a headless browser under the hood.
68
+ - `HandelsregisterClient` needs a gate. The portal's terms allow at most 60 searches or retrievals of entities per hour from one installation, so share one `SlidingWindowGate` (or a gate of your own) among every client that reaches the portal from the same address.
69
+ - The portal's terms allow single lookups for information purposes. Systematic retrieval to build or update a copy of the registers is not allowed, and neither is publishing the data under the name "Handelsregister" (§ 8 Abs. 2 HGB).
61
70
  - `StockDataService` defaults to a 24 hour price cache. If you want fresher quote caching, pass `cache.priceTTL` explicitly or use `StockPriceService` directly.
62
71
 
63
72
  ## Quick Start
@@ -194,118 +203,176 @@ console.log({
194
203
  });
195
204
  ```
196
205
 
197
- ### German Company Data And Handelsregister Automation
206
+ ### Register Lookups (Handelsregister)
198
207
 
199
- `OpenData` is the local workflow layer for German company data. It creates its directories, boots an embedded SmartDB-backed database, and starts a browser automation session for Handelsregister access.
208
+ `HandelsregisterClient` speaks to the register portal of the federal states over plain HTTP, the way a browser would: it opens the search form, sends the search, reads the result list and, for one entry, downloads the structured register content (SI, an XJustiz XML file) and parses it.
200
209
 
201
210
  ```ts
202
- import { OpenData } from '@fin.cx/opendata';
203
- import * as path from 'node:path';
211
+ import {
212
+ HandelsregisterClient,
213
+ HandelsregisterError,
214
+ SlidingWindowGate,
215
+ findRegisterCourt,
216
+ formatRegisterReference,
217
+ } from '@fin.cx/opendata/handelsregister';
218
+
219
+ // one gate for the whole installation: 60 searches or retrievals per hour
220
+ const gate = new SlidingWindowGate();
221
+
222
+ const register = new HandelsregisterClient({
223
+ gate,
224
+ userAgent: 'Example Books (+https://example.com/contact)',
225
+ });
204
226
 
205
- const nogitDir = path.join(process.cwd(), '.nogit');
227
+ // by name: every row the portal lists, in its order, never narrowed down to one
228
+ const found = await register.search({ name: 'Task Venture Capital', seat: 'Bremen' });
229
+ const choices = found.matches.map((match) => ({
230
+ reference: formatRegisterReference(match.reference), // 'Amtsgericht Bremen HRB 35230 HB'
231
+ name: match.name,
232
+ sheet: match.sheetStatus, // 'current' or 'closed'
233
+ formerNames: match.formerNames,
234
+ }));
235
+ if (found.truncated) {
236
+ // the portal stops at 100 hits: add the seat, the court or the register number
237
+ }
238
+
239
+ // one register sheet, with its structured content
240
+ const court = findRegisterCourt('Amtsgericht Bremen');
241
+ if (court) {
242
+ const entry = await register.getEntry({ courtCode: court.code, type: 'HRB', number: '35230 HB' });
243
+ const content = entry.structuredContent; // null when the portal offers none for this entry
244
+ if (content) {
245
+ const facts = {
246
+ name: content.name,
247
+ legalForm: content.legalForm?.short, // 'GmbH'
248
+ address: content.address,
249
+ status: content.status?.state, // 'active', 'deleted', 'in_liquidation', 'insolvent', 'merged'
250
+ representatives: content.representatives, // [{ name, function, functionCode, kind }]
251
+ retrievedAt: entry.retrievedAt,
252
+ notice: entry.notice.en, // the portal's note: the structured content is not binding
253
+ };
254
+ }
255
+ }
256
+
257
+ // every failure is typed
258
+ try {
259
+ await register.getEntry({ courtCode: 'H1101', type: 'HRB', number: '35230' });
260
+ } catch (error) {
261
+ if (error instanceof HandelsregisterError && error.code === 'rate_limited') {
262
+ const nextPossibleAt = new Date(error.retryAt ?? Date.now());
263
+ }
264
+ }
265
+ ```
206
266
 
207
- const openData = new OpenData({
208
- nogitDir,
209
- downloadDir: path.join(nogitDir, 'downloads'),
210
- germanBusinessDataDir: path.join(nogitDir, 'germanbusinessdata'),
211
- });
267
+ Searches take a name or a register number:
212
268
 
213
- await openData.start();
269
+ - `{ name, match?: 'all' | 'any' | 'exact', seat?, type?, courtCode?, includeClosed? }`
270
+ - `{ type, number, courtCode?, includeClosed? }` where `type` is `HRA`, `HRB`, `GnR`, `PR`, `VR` or `GsR` and `number` may carry the location suffix (`999924 B`, `999928 NM`). The portal is searched by the digits, so rows with any suffix come back. Without `courtCode`, every court's entry with that number is returned.
214
271
 
215
- const matches = await openData.handelsregister.searchCompany('Siemens AG', 20);
272
+ `getEntry({ courtCode, type, number })` identifies one sheet. It includes closed sheets, compares the location suffix when you give one, and never picks one of several: it fails with `not_found` or `ambiguous` and lists the candidates.
216
273
 
217
- const company = await openData.handelsregister.getSpecificCompany({
218
- court: 'Munich',
219
- type: 'HRB',
220
- number: '6684',
221
- });
274
+ #### What counts against the 60 per hour
222
275
 
223
- console.log({
224
- searchHits: matches.length,
225
- downloadedFiles: company.files.map((file) => file.path),
226
- });
276
+ | Call | Counted requests | Also loaded |
277
+ | --- | --- | --- |
278
+ | `search()` | 1 search | the search form and the result list |
279
+ | `getEntry()` | 1 search, plus 1 retrieval when the portal offers structured content | the search form and the result list |
227
280
 
228
- await openData.stop();
229
- ```
281
+ The client asks the gate before each counted request and before that step sends anything, so a refusal costs no traffic. A slot taken for a step that then fails before its request is sent stays taken; the count errs on the safe side. The client never repeats a request by itself, never caches, and never writes to the console. Every call opens its own visit to the portal and forgets it afterwards.
230
282
 
231
- Useful `OpenData` workflows today:
283
+ A refusal of `SlidingWindowGate` is a `HandelsregisterError` with the code `rate_limited` and `retryAt`, the moment the next request is possible. Like every error of the client it carries the refused `step` (`search` or `structured_content`) and the `countedRequests` made before it: a `getEntry()` refused at the retrieval has already counted its search. A gate of your own may refuse with any error; a `HandelsregisterError` gains the step and the count the same way, and any other error reaches the caller unchanged.
232
284
 
233
- - `await openData.start()`
234
- - `await openData.buildInitialDb()` to ingest the public German company JSONL dataset
235
- - `await openData.handelsregister.searchCompany(name, limit)`
236
- - `await openData.handelsregister.getSpecificCompany({ court, type, number })`
237
- - `await openData.handelsregister.getSpecificCompanyByName(name)`
285
+ #### What a result holds
238
286
 
239
- The download workflow currently fetches the Handelsregister files into a temporary folder and returns the files to you as `SmartFile` objects.
287
+ - `IRegisterMatch`, one row of the result list: `reference` (court with code and name, register type, number with suffix), `formerCourt`, `federalState`, `name`, `seat`, `sheetStatus` (`current`, `closed` or `other`, with the portal's word in `sheetStatusText`), `formerNames` and the `documents` the portal offers.
288
+ - `IRegisterStructuredContent`, the parsed SI: `name`, `legalForm` (XUnternehmen code with short and long designation), `seat`, the business `address`, `register`, `status` (XJustiz status of the legal entity), `lastEntryOn`, `representatives` and `representationStated`, `producedAt`, `binding: false` and the portal's `notice`.
289
+ - A closed sheet ("Geschlossenes Registerblatt") means the entry was deleted, or its sheet was closed when the entity moved to another court, changed its legal form or merged. The result list does not say which, and the portal offers no structured content for it.
290
+ - The structured content states no date for the status itself; `lastEntryOn` is the date of the latest entry on the sheet.
291
+ - Of people the parser reads the name and the function only. Birth dates, places of residence and private addresses never leave it.
292
+ - Codes are decoded with tables taken from the code lists on XRepository on 2026-09-23. A code the tables do not know is kept as a code, without a designation.
240
293
 
241
- ### Law Syncing And Search
294
+ #### What can happen
242
295
 
243
- `LawService` stores synced laws in a local embedded database and supports three jurisdictions:
296
+ | Case | Result |
297
+ | --- | --- |
298
+ | No match | `search()` returns no matches; `getEntry()` fails with `not_found` |
299
+ | Several matches in different courts | every row, in the portal's order |
300
+ | More than 100 hits | the first 100 and `truncated: true` |
301
+ | Renamed entity | the current name, earlier names in `formerNames` |
302
+ | Deleted or moved entity | `sheetStatus: 'closed'`, no structured content |
303
+ | Dissolved or insolvent entity | `status.state` `in_liquidation` or `insolvent`, and the liquidators among the representatives |
304
+ | Branch of a company under foreign law | its foreign legal form and seat, and its permanent representatives |
305
+ | Partnership, cooperative, association, civil-law partnership | register types `PR`, `GnR`, `VR`, `GsR` |
306
+ | Number without suffix that fits several sheets | `ambiguous` with the candidates |
307
+ | Suffix that fits no sheet | `not_found` with the sheets of the same number |
308
+ | Structured content of another sheet | `unexpected_response` |
309
+ | Portal answers with an HTTP error (blocked address, overload) | `refused` with `status` |
310
+ | Portal shows its notice that it refuses retrievals from this address | `refused` with its words in `portalMessage` |
311
+ | Portal too slow, unreachable, or call aborted | `timeout`, `unreachable`, `aborted` |
312
+ | Portal changed its pages or answered in another language | `unexpected_response`, with the portal's own message in `portalMessage` when it showed one |
313
+ | Structured content that cannot be read | `invalid_structured_content` |
244
314
 
245
- - Germany via `gesetze-im-internet`
246
- - EU via `EUR-Lex`
247
- - US via GovInfo and Cornell LII
315
+ Errors of the client carry `countedRequests` and the `step` that failed, so you can say whether a request may have counted even when its answer did not arrive. Error messages contain no register data.
316
+
317
+ #### Parsing without a request
248
318
 
249
319
  ```ts
250
- import { LawService } from '@fin.cx/opendata';
320
+ import { parseStructuredContent } from '@fin.cx/opendata/handelsregister';
251
321
 
252
- const laws = new LawService({
253
- dbFolderPath: '.nogit/law-smartdb',
254
- dbName: 'laws',
255
- govInfoApiKey: process.env.GOVINFO_API_KEY,
256
- });
322
+ // an SI file a person downloaded from the portal themselves
323
+ const content = parseStructuredContent(xmlText);
324
+ ```
257
325
 
258
- await laws.start();
326
+ #### Printed court names
259
327
 
260
- const germanLaw = await laws.syncLaw({
261
- jurisdiction: 'de',
262
- identifier: 'aeg',
263
- });
328
+ `findRegisterCourt()` takes a court's code or its name as the portal or a business prints it: `AG Bremen`, `Amtsgericht Bremen – Registergericht`, `Registergericht Bremen`, `Handelsregister B des Amtsgerichts Bremen`, `Amtsgericht Frankfurt a. M.`, `Frankfurt/O.`, `Kempten` for `Kempten (Allgäu)`, `Freiburg i. Br.`, and `AG Berlin` or `Charlottenburg` for the register court of Berlin. Umlauts may be spelled out (`Muenchen`). It returns a court only when exactly one fits: `Frankfurt` alone could be either Frankfurt am Main or Frankfurt (Oder) and names no court.
264
329
 
265
- const euLaw = await laws.syncLaw({
266
- jurisdiction: 'eu',
267
- identifier: '32024R1689',
268
- language: 'EN',
269
- });
330
+ ### Public Holidays
270
331
 
271
- const usCode = await laws.syncLaw({
272
- jurisdiction: 'us',
273
- identifier: '8 U.S.C. § 1226',
274
- });
332
+ ```ts
333
+ import {
334
+ getPublicHolidays,
335
+ isWorkingDay,
336
+ nextWorkingDay,
337
+ workingDayOnOrAfter,
338
+ } from '@fin.cx/opendata/holidays';
275
339
 
276
- const results = await laws.searchLaws({
277
- jurisdiction: 'eu',
278
- query: 'Artificial Intelligence Act',
279
- limit: 5,
280
- });
340
+ getPublicHolidays(2026, 'DE-NI');
341
+ // [{ date: '2026-01-01', id: 'neujahr', name: 'Neujahr', state: 'DE-NI', scope: 'state',
342
+ // legalBasis: '§ 2 Abs. 1 NFeiertagsG' }, …, { date: '2026-10-31', id: 'reformationstag', … }, …]
281
343
 
282
- console.log({
283
- germany: germanLaw.title,
284
- eu: euLaw.title,
285
- us: usCode.title,
286
- hits: results.length,
287
- });
344
+ isWorkingDay('2026-06-04', 'DE-HE', { regionalHolidays: 'ignore' }); // false: Fronleichnam
345
+ nextWorkingDay('2026-04-02', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-04-07'
288
346
 
289
- await laws.stop();
347
+ // § 193 BGB: a deadline that ends on a Saturday, Sunday or holiday ends on the next working day
348
+ workingDayOnOrAfter('2026-05-14', 'DE-BY', { regionalHolidays: 'ignore' }); // '2026-05-15'
290
349
  ```
291
350
 
292
- `LawService` also supports bulk sync:
351
+ States are ISO 3166-2 codes (`DE-BY`); `findGermanState('Bayern')` and `findGermanState('BY')` find one. Dates are plain calendar dates written `YYYY-MM-DD`, so no time zone can move them.
293
352
 
294
- ```ts
295
- await laws.syncLaws({
296
- jurisdiction: 'us',
297
- usCollection: 'PLAW',
298
- limit: 25,
299
- since: new Date('2025-01-01'),
300
- });
301
- ```
353
+ #### Regional holidays
302
354
 
303
- Useful request patterns:
355
+ Four holidays apply only in part of a state:
304
356
 
305
- - Germany: `identifier: 'aeg'`
306
- - EU: `identifier: '32024R1689'`, `language: 'EN'`
307
- - US public law package: `identifier: 'PLAW-119publ1'`, `usCollection: 'PLAW'`
308
- - US Code citation: `identifier: '8 U.S.C. § 1226'`
357
+ | Holiday | State | Where | Legal basis |
358
+ | --- | --- | --- | --- |
359
+ | Augsburger Friedensfest (8 August) | Bayern | Stadt Augsburg | Art. 1 Abs. 2 FTG |
360
+ | Mariä Himmelfahrt (15 August) | Bayern | municipalities with a predominantly Catholic population, as the Bayerisches Landesamt für Statistik determines them after the last census | Art. 1 Abs. 1 Nr. 2, Abs. 3 FTG |
361
+ | Fronleichnam | Sachsen | the municipalities listed in the annex of the FronleichnamsVO (former districts Bautzen, Hoyerswerda and Kamenz), after a merger only the respective part of the municipality | § 1 Abs. 1 SächsSFG, § 1 FronleichnamsVO |
362
+ | Fronleichnam | Thüringen | the parts of Thüringen where Fronleichnam was a statutory holiday in 1994 | § 2 Abs. 2, § 10 Abs. 1 ThürFGtG |
363
+
364
+ `getPublicHolidays()` lists them with `scope: 'regional'` and the `region` they apply to. The calendar holds no list of these municipalities: the Bavarian list follows the census and the Saxon and Thuringian ones name parts of municipalities, so a place cannot be matched to them reliably by a key. Every working-day question therefore says how to treat them:
365
+
366
+ - `{ regionalHolidays: 'count' }` treats them as holidays. A period then never ends on a day that is a holiday at the place in question, at the price of ending one day later where it is not. Use it when the place is unknown.
367
+ - `{ regionalHolidays: 'ignore' }` treats them as working days, when the place is known to lie outside that part.
368
+
369
+ #### What the calendar covers
370
+
371
+ - The statutory public holidays of the 16 federal states from 2018 on, and 3 October from Art. 2 Abs. 2 of the Unification Treaty. Every change since 2018 applies from its year: Reformationstag in Bremen, Hamburg, Niedersachsen and Schleswig-Holstein (2018), Frauentag in Berlin (2019) and Mecklenburg-Vorpommern (2023), Weltkindertag in Thüringen (2019), and the one-off holidays of Berlin on 8 May 2020, 8 May 2025 and 17 June 2028. Years before 2018 end in `HolidayCalendarError` with the code `unsupported_year`.
372
+ - Later years follow the laws as they stood when this version read them (`holidayLawsReadOn`). Several states allow their government to declare one-off holidays by ordinance; one declared after that day needs a new version.
373
+ - The Easter-based holidays follow Easter Sunday of the Gregorian calendar; Buß- und Bettag (Sachsen) is the Wednesday before the last Sunday before the first Sunday of Advent.
374
+ - The laws of Berlin and Hessen also count every Sunday as a public holiday, and Brandenburg names Ostersonntag and Pfingstsonntag. The calendar lists the Sundays Brandenburg names; for working days every Sunday is excluded anyway.
375
+ - Religious holidays that protect only church services or give employees time off (for example Fronleichnam outside its region in Sachsen, § 3 SächsSFG) and memorial days (Gedenktage) are no statutory public holidays and are not listed.
309
376
 
310
377
  ## API At A Glance
311
378
 
@@ -346,33 +413,97 @@ Useful request patterns:
346
413
  - `getProviderStats()`
347
414
  - `clearCache()`
348
415
 
349
- ### `OpenData`
350
-
351
- - `start()`
352
- - `stop()`
353
- - `buildInitialDb()`
354
- - `handelsregister.searchCompany(name, limit?)`
355
- - `handelsregister.getSpecificCompany({ court, type, number })`
356
- - `handelsregister.getSpecificCompanyByName(name)`
357
-
358
- ### `LawService`
359
-
360
- - `start()`
361
- - `stop()`
362
- - `getLaw(request)`
363
- - `syncLaw(request)`
364
- - `syncLaws(request)`
365
- - `searchLaws({ query, jurisdiction?, limit? })`
416
+ ### `HandelsregisterClient` (`@fin.cx/opendata/handelsregister`)
417
+
418
+ - `new HandelsregisterClient({ gate, portalUrl?, userAgent?, requestTimeoutMs?, maxResponseBytes?, now? })`
419
+ - `search(query, { signal? })`
420
+ - `getEntry({ courtCode, type, number }, { signal? })`
421
+
422
+ ### `SlidingWindowGate`
423
+
424
+ - `new SlidingWindowGate({ limit?, windowMs?, now? })`
425
+ - `acquire({ kind })`
426
+ - `remaining()`
427
+ - `nextAllowedAt()`
428
+
429
+ ### Register helpers
430
+
431
+ - `parseStructuredContent(xml)`
432
+ - `parseSearchResults(html)`
433
+ - `registerCourts`
434
+ - `findRegisterCourt(codeOrName)`
435
+ - `parseRegisterReferenceText(text)`
436
+ - `parseRegisterNumber(text)`
437
+ - `formatRegisterReference(reference)`
438
+ - `structuredContentNotice`
439
+
440
+ ### Holiday calendar (`@fin.cx/opendata/holidays`)
441
+
442
+ - `getPublicHolidays(year, state)`
443
+ - `getPublicHolidaysOn(date, state)`
444
+ - `isPublicHoliday(date, state, { regionalHolidays })`
445
+ - `isWorkingDay(date, state, { regionalHolidays })`
446
+ - `nextWorkingDay(date, state, { regionalHolidays })`
447
+ - `workingDayOnOrAfter(date, state, { regionalHolidays })`
448
+ - `easterSunday(year)`
449
+ - `germanStates`, `findGermanState(codeOrName)`
450
+ - `holidayCalendarFirstYear`, `holidayLawsReadOn`
451
+ - `HolidayCalendarError` with the codes `invalid_date`, `unsupported_year`, `unknown_state` and `invalid_options`
452
+
453
+ ## Data Sources
454
+
455
+ The package ships no market or register data. Prices and fundamentals come at run time from the providers you register (Marketstack, CoinGecko, SEC EDGAR), and register entries come at run time from the register portal of the federal states (handelsregister.de), one lookup at a time.
456
+
457
+ The package carries two kinds of data: the holiday rules of `@fin.cx/opendata/holidays` (`ts/holidays/data.holidays.ts`), described at the end of this section, and the code tables that `@fin.cx/opendata/handelsregister` uses to decode the structured register content (`ts/handelsregister/data.codelists.ts`). The code tables are official code lists, published on XRepository (https://www.xrepository.de), and were taken on 2026-09-23:
458
+
459
+ | Table | Code list | Issued by |
460
+ | --- | --- | --- |
461
+ | `legalFormLabels` | `urn:xoev-de:xunternehmen:codeliste:rechtsformen_2.4` (Rechtsformen) | XUnternehmen |
462
+ | `roleLabels` | `urn:xoev-de:xjustiz:codeliste:gds.rollenbezeichnung_3.6` (GDS.Rollenbezeichnung) | BLK-AG IT-Standards in der Justiz |
463
+ | `entityStatusLabels` | `urn:xoev-de:xjustiz:codeliste:reg.status-rechtstraeger_2.0` (REG.Status_Rechtstraeger) | BLK-AG IT-Standards in der Justiz |
464
+ | `stateKeyToIso` | `urn:xoev-de:bund:bfj:codeliste:bfj.staat_7.0` (BfJ Staat) | Bundesamt für Justiz, Bonn |
465
+ | `registerCourts` | the register courts of the portal's search form, with their codes from `urn:xoev-de:xjustiz:codeliste:gds.gerichte` | BLK-AG IT-Standards in der Justiz |
466
+
467
+ Codes and designations are reproduced as published, without changes. The tables hold a selection of each list, never an edited entry:
468
+
469
+ - `legalFormLabels`: the concrete legal forms that one of the justice registers can carry, with the short and the long designation;
470
+ - `roleLabels`: the 71 roles that occur in register content;
471
+ - `entityStatusLabels`: the whole list;
472
+ - `stateKeyToIso`: the key and the ISO 3166-1 alpha-2 code of each state or territory that has one;
473
+ - `registerCourts`: the 150 register courts as the portal's search form named them on the same day.
474
+
475
+ A later version of a code list may add codes. The parser keeps a code it does not know as a code, without a designation, and never guesses one.
476
+
477
+ The holiday calendar is derived from the holiday laws of the federal states, read on 2026-09-24 in their official portals, from Art. 2 Abs. 2 of the Unification Treaty for 3 October, and it answers the question of § 193 BGB:
478
+
479
+ | State | Law | Provision |
480
+ | --- | --- | --- |
481
+ | all | [Einigungsvertrag](https://www.gesetze-im-internet.de/einigvtr/art_2.html) | Art. 2 Abs. 2 (Tag der Deutschen Einheit) |
482
+ | Baden-Württemberg | [Feiertagsgesetz (FTG)](https://www.landesrecht-bw.de/perma?j=FeiertG_BW_!_1) | § 1 |
483
+ | Bayern | [Feiertagsgesetz (FTG)](https://www.gesetze-bayern.de/Content/Document/BayFTG-1) | Art. 1 |
484
+ | Berlin | [Gesetz über die Sonn- und Feiertage](https://gesetze.berlin.de/perma?j=FeiertG_BE_!_1) | § 1 Abs. 1 |
485
+ | Brandenburg | [Feiertagsgesetz (FTG)](https://bravors.brandenburg.de/gesetze/ftg_2015) | § 2 Abs. 1 |
486
+ | Bremen | [Gesetz über die Sonn-, Gedenk- und Feiertage](https://www.transparenz.bremen.de/sixcms/detail.php?gsid=bremen2014_tp.c.145882.de) | § 2 Abs. 1 |
487
+ | Hamburg | [Feiertagsgesetz](https://www.landesrecht-hamburg.de/perma?d=jlr-NNLHH00006CB9NN00000000002) | § 1 |
488
+ | Hessen | [Hessisches Feiertagsgesetz (HFeiertagsG)](https://www.rv.hessenrecht.hessen.de/perma?j=FeiertG_HE_!_1) | § 1 Abs. 1 |
489
+ | Mecklenburg-Vorpommern | [Feiertagsgesetz (FTG M-V)](https://www.landesrecht-mv.de/perma?d=jlr-NNLMV00007A2FNN00000000003) | § 2 Abs. 1 |
490
+ | Niedersachsen | [Niedersächsisches Gesetz über die Feiertage (NFeiertagsG)](https://voris.wolterskluwer-online.de/browse/document/f74bc6e7-6ded-3c2c-9afb-b34620456e56) | § 2 Abs. 1 |
491
+ | Nordrhein-Westfalen | [Gesetz über die Sonn- und Feiertage (Feiertagsgesetz NW)](https://recht.nrw.de/lrgv/gesetz/01012000-bekanntmachung-der-neufassung-des-gesetzes-ueber-die-sonn-und-feiertage) | § 2 Abs. 1 |
492
+ | Rheinland-Pfalz | [Feiertagsgesetz (LFtG)](https://landesrecht.rlp.de/perma?d=jlr-NNLRP00006125NN00000000004) | § 2 Abs. 1 |
493
+ | Saarland | [Feiertagsgesetz (SFG)](https://recht.saarland.de/perma?d=jlr-NNLSL0000A2D6NN00000000007) | § 2 Abs. 1 |
494
+ | Sachsen | [Sächsisches Sonn- und Feiertagsgesetz (SächsSFG)](https://www.revosax.sachsen.de/vorschrift/3997-SaechsSFG), [FronleichnamsVO](https://www.revosax.sachsen.de/vorschrift/4142) | § 1 Abs. 1; § 1 and annex |
495
+ | Sachsen-Anhalt | [Gesetz über die Sonn- und Feiertage (FeiertG LSA)](https://www.landesrecht.sachsen-anhalt.de/perma?d=jlr-NNLST0000411CNN00000000003) | § 2 |
496
+ | Schleswig-Holstein | [Gesetz über Sonn- und Feiertage (SFTG)](https://www.gesetze-rechtsprechung.sh.juris.de/perma?j=FeiertG_SH_!_2) | § 2 Abs. 1 |
497
+ | Thüringen | [Thüringer Feier- und Gedenktagsgesetz (ThürFGtG)](https://landesrecht.thueringen.de/perma?j=FeiertG_TH_!_2) | § 2, § 10 Abs. 1 |
498
+ | all | [Bürgerliches Gesetzbuch](https://www.gesetze-im-internet.de/bgb/__193.html) | § 193 (Sonntag, Feiertag, Sonnabend) |
366
499
 
367
500
  ## Behavior And Caveats
368
501
 
369
502
  - Historical market data is cached aggressively because it is treated as immutable.
370
503
  - Intraday data uses interval-aware caching and tries incremental refreshes for recent data.
371
504
  - `StockDataService` is convenience-first. `StockPriceService` is the better low-level choice if you care about request-shape-specific cache behavior.
372
- - `OpenData` is a real workflow layer, not just types. It writes local data, launches a browser, and manages downloads.
373
- - Handelsregister and EUR-Lex integrations depend on current site structure. They are practical automation workflows, not guaranteed stable official APIs.
374
- - The most useful public `OpenData` features today are startup, JSONL import, and Handelsregister operations. Some extra validation/search helper methods exist in source but are still stubs.
375
- - `LawService` persists synced laws locally, so repeated searches can stay offline once the data has been synced.
505
+ - The register lookup reads the portal's German pages and asks for German explicitly. It depends on the portal's current page structure: when that changes, calls end in `unexpected_response` rather than in guessed data.
506
+ - The structured register content is a non-binding service of the portal; the current printout is binding. Results carry `binding: false` and the portal's note.
376
507
  - There is no hosted backend in this package. Everything is meant to run from your own TypeScript/Node process.
377
508
 
378
509
  ## Testing
@@ -381,7 +512,7 @@ Useful request patterns:
381
512
  pnpm test
382
513
  ```
383
514
 
384
- Some tests hit live remote services, so they may depend on network access, optional API credentials, or browser automation support.
515
+ Some market data tests hit live remote services, so they may depend on network access or optional API credentials. The register lookup is tested against minimal hand-written pages and structured content with synthetic values only; the tests never contact the register portal.
385
516
 
386
517
  ## License and Legal Information
387
518
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@fin.cx/opendata',
6
- version: '3.6.0',
7
- description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.'
6
+ version: '4.1.0',
7
+ description: 'Open business and financial data for TypeScript: register lookups on the German register portal (handelsregister.de) without storage, the statutory public holidays of the German federal states, and stock and crypto prices and fundamentals from several providers.'
8
8
  }