@code-plate/iran-cities 1.1.0 → 2.0.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 (130) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.fa.md +277 -0
  4. package/MIGRATION.md +272 -0
  5. package/NOTICE.md +38 -0
  6. package/README.fa.md +528 -201
  7. package/README.md +524 -201
  8. package/dist/boundaries.cjs +9218 -0
  9. package/dist/boundaries.cjs.map +1 -0
  10. package/dist/boundaries.d.cts +30 -0
  11. package/dist/boundaries.d.mts +30 -0
  12. package/dist/boundaries.mjs +9215 -0
  13. package/dist/boundaries.mjs.map +1 -0
  14. package/dist/changes-B-wz9-Gy.cjs +841 -0
  15. package/dist/changes-B-wz9-Gy.cjs.map +1 -0
  16. package/dist/changes-CMVZbfZO.d.mts +8 -0
  17. package/dist/changes-DLZkHBwZ.mjs +800 -0
  18. package/dist/changes-DLZkHBwZ.mjs.map +1 -0
  19. package/dist/changes-DnnwRAKl.d.cts +8 -0
  20. package/dist/components-B7060em0.mjs +240 -0
  21. package/dist/components-B7060em0.mjs.map +1 -0
  22. package/dist/components-BuMUB7XT.d.cts +47 -0
  23. package/dist/components-DZz72pd1.d.mts +47 -0
  24. package/dist/components-grWmOsaV.cjs +247 -0
  25. package/dist/components-grWmOsaV.cjs.map +1 -0
  26. package/dist/components.css +251 -0
  27. package/dist/data/cities.csv +1253 -0
  28. package/dist/data/cities.json +23305 -0
  29. package/dist/data/iran-cities.sql +1487 -0
  30. package/dist/data/iran-provinces.geojson +1 -0
  31. package/dist/data/provinces.csv +32 -0
  32. package/dist/data/provinces.json +460 -0
  33. package/dist/hooks-C6kjWLIE.mjs +134 -0
  34. package/dist/hooks-C6kjWLIE.mjs.map +1 -0
  35. package/dist/hooks-K7pTMF2R.cjs +139 -0
  36. package/dist/hooks-K7pTMF2R.cjs.map +1 -0
  37. package/dist/index.cjs +30 -7550
  38. package/dist/index.d.cts +25 -0
  39. package/dist/index.d.mts +25 -25
  40. package/dist/index.mjs +3 -7507
  41. package/dist/lite-CtNPRhU-.mjs +1926 -0
  42. package/dist/lite-CtNPRhU-.mjs.map +1 -0
  43. package/dist/lite-CutarzAc.cjs +2051 -0
  44. package/dist/lite-CutarzAc.cjs.map +1 -0
  45. package/dist/lite-react-1HVWJOWt.cjs +55 -0
  46. package/dist/lite-react-1HVWJOWt.cjs.map +1 -0
  47. package/dist/lite-react-Ds-DNmEi.mjs +20 -0
  48. package/dist/lite-react-Ds-DNmEi.mjs.map +1 -0
  49. package/dist/lite-react-components.cjs +12 -0
  50. package/dist/lite-react-components.cjs.map +1 -0
  51. package/dist/lite-react-components.d.cts +5 -0
  52. package/dist/lite-react-components.d.mts +5 -0
  53. package/dist/lite-react-components.mjs +11 -0
  54. package/dist/lite-react-components.mjs.map +1 -0
  55. package/dist/lite-react.cjs +8 -0
  56. package/dist/lite-react.d.cts +7 -0
  57. package/dist/lite-react.d.mts +7 -0
  58. package/dist/lite-react.mjs +2 -0
  59. package/dist/lite.cjs +28 -0
  60. package/dist/lite.d.cts +23 -0
  61. package/dist/lite.d.mts +23 -0
  62. package/dist/lite.mjs +3 -0
  63. package/dist/react-BKmJ476x.mjs +20 -0
  64. package/dist/react-BKmJ476x.mjs.map +1 -0
  65. package/dist/react-BZV3TkXB.cjs +55 -0
  66. package/dist/react-BZV3TkXB.cjs.map +1 -0
  67. package/dist/react-components.cjs +12 -0
  68. package/dist/react-components.cjs.map +1 -0
  69. package/dist/react-components.d.cts +5 -0
  70. package/dist/react-components.d.mts +5 -0
  71. package/dist/react-components.mjs +11 -0
  72. package/dist/react-components.mjs.map +1 -0
  73. package/dist/react.cjs +8 -7553
  74. package/dist/react.d.cts +7 -0
  75. package/dist/react.d.mts +7 -30
  76. package/dist/react.mjs +2 -7524
  77. package/dist/rolldown-runtime-78lK1XfQ.cjs +42 -0
  78. package/dist/rolldown-runtime-D3KUYPsf.mjs +12 -0
  79. package/dist/src-DRaUVbc4.mjs +4302 -0
  80. package/dist/src-DRaUVbc4.mjs.map +1 -0
  81. package/dist/src-mqZTwJfQ.cjs +4439 -0
  82. package/dist/src-mqZTwJfQ.cjs.map +1 -0
  83. package/dist/types/boundaries.d.ts +30 -0
  84. package/dist/types/changes.d.ts +4 -0
  85. package/dist/types/components.d.ts +47 -0
  86. package/dist/types/core.d.ts +25 -0
  87. package/dist/types/geo.d.ts +2 -0
  88. package/dist/types/hooks.d.ts +2 -0
  89. package/dist/types/index.d.ts +28 -0
  90. package/dist/types/keyboard.d.ts +3 -0
  91. package/dist/types/lite-react-components.d.ts +5 -0
  92. package/dist/types/lite-react.d.ts +7 -0
  93. package/dist/types/lite.d.ts +26 -0
  94. package/dist/types/normalize.d.ts +1 -0
  95. package/dist/types/packed.d.ts +16 -0
  96. package/dist/types/react-components.d.ts +5 -0
  97. package/dist/types/react.d.ts +7 -0
  98. package/dist/types/types.d.ts +149 -0
  99. package/dist/types/valibot.d.ts +23 -0
  100. package/dist/types/validation-shared.d.ts +11 -0
  101. package/dist/types/zod.d.ts +23 -0
  102. package/dist/types-Ds_b0yHP.d.cts +150 -0
  103. package/dist/types-Ds_b0yHP.d.mts +150 -0
  104. package/dist/valibot.cjs +47 -0
  105. package/dist/valibot.cjs.map +1 -0
  106. package/dist/valibot.d.cts +21 -0
  107. package/dist/valibot.d.mts +21 -0
  108. package/dist/valibot.mjs +38 -0
  109. package/dist/valibot.mjs.map +1 -0
  110. package/dist/validation-shared-BFilTuY1.cjs +58 -0
  111. package/dist/validation-shared-BFilTuY1.cjs.map +1 -0
  112. package/dist/validation-shared-C7l2cv4e.d.cts +8 -0
  113. package/dist/validation-shared-CJjGRx8p.d.mts +7 -0
  114. package/dist/validation-shared-mh_saZne.mjs +29 -0
  115. package/dist/validation-shared-mh_saZne.mjs.map +1 -0
  116. package/dist/zod.cjs +49 -0
  117. package/dist/zod.cjs.map +1 -0
  118. package/dist/zod.d.cts +21 -0
  119. package/dist/zod.d.mts +21 -0
  120. package/dist/zod.mjs +42 -0
  121. package/dist/zod.mjs.map +1 -0
  122. package/package.json +177 -29
  123. package/dist/index.cjs.map +0 -1
  124. package/dist/index.d.ts +0 -25
  125. package/dist/index.mjs.map +0 -1
  126. package/dist/react.cjs.map +0 -1
  127. package/dist/react.d.ts +0 -30
  128. package/dist/react.mjs.map +0 -1
  129. package/dist/types-DzSw8Uw2.d.mts +0 -52
  130. package/dist/types-DzSw8Uw2.d.ts +0 -52
package/MIGRATION.md ADDED
@@ -0,0 +1,272 @@
1
+ English | [فارسی](./MIGRATION.fa.md)
2
+
3
+ # Migrating from 1.x to 2.0
4
+
5
+ Version 2.0 is mostly a drop-in upgrade. Plain JavaScript code, Node.js/Express backends and typical React/Next.js forms keep working without any change. This guide lists **every** difference that can affect existing code, how to find out whether your project is affected, and the exact fix.
6
+
7
+ Each item was verified by running the same calls against the 1.2.0 build and the 2.0.0 build.
8
+
9
+ ## Contents
10
+
11
+ - [Quick checklist](#quick-checklist)
12
+ - [What did not change](#what-did-not-change)
13
+ - [Breaking changes](#breaking-changes)
14
+ - [1. Returned data is frozen and array types are `readonly`](#1-returned-data-is-frozen-and-array-types-are-readonly)
15
+ - [2. `getCity(name)` picks a different city for 7 shared names](#2-getcityname-picks-a-different-city-for-7-shared-names)
16
+ - [3. New required fields on the data types](#3-new-required-fields-on-the-data-types)
17
+ - [4. 25 Persian names now use Persian letters](#4-25-persian-names-now-use-persian-letters)
18
+ - [Behaviour changes that are not breaking](#behaviour-changes-that-are-not-breaking)
19
+ - [New features you can adopt](#new-features-you-can-adopt)
20
+ - [Next.js](#nextjs)
21
+ - [Finding affected code](#finding-affected-code)
22
+ - [FAQ](#faq)
23
+
24
+ ## Quick checklist
25
+
26
+ 1. `npm install @code-plate/iran-cities@^2`
27
+ 2. Run your TypeScript compiler. Errors of the form `TS4104: The type 'readonly City[]' is 'readonly' and cannot be assigned to the mutable type 'City[]'` are [item 1](#1-returned-data-is-frozen-and-array-types-are-readonly); `Property 'aliases' is missing` (or `nameEn`, `neighborIds`, `provinceNameEn`) is [item 3](#3-new-required-fields-on-the-data-types).
28
+ 3. Search your code for code that **modifies** data returned by the package (`.sort(`, `.push(`, `.splice(`, `.reverse(`, assignments to `city.xxx =`). See [Finding affected code](#finding-affected-code).
29
+ 4. If you call `getCity()` or `getProvinceOfCity()` with a name but without a province, check the [7 names](#2-getcityname-picks-a-different-city-for-7-shared-names).
30
+ 5. If you compare `city.fa` with Persian names stored in your database, check [item 4](#4-25-persian-names-now-use-persian-letters).
31
+ 6. Optional: switch client components to `@code-plate/iran-cities/lite/react` to cut the client bundle by about a third.
32
+
33
+ ## What did not change
34
+
35
+ - **Supported Node.js versions:** `engines.node` is still `>=14`. The published files are tested on Node.js 14, 16, 18, 20, 22 and 24, as CommonJS and as ES modules.
36
+ - **Supported TypeScript versions:** the type declarations work from TypeScript 3.9 up to 7.x, with `moduleResolution` set to `node`/`node10`, `node16`/`nodenext` or `bundler`.
37
+ - **React peer dependency:** still `>=16.8.0`, still optional.
38
+ - **Import paths:** `@code-plate/iran-cities` and `@code-plate/iran-cities/react` are unchanged. Two subpaths were added; none were removed.
39
+ - **Every 1.x function and every exported type still exists** with the same name and the same parameters.
40
+ - **Ids, slugs and data:** city and province ids, English ids, slugs, area codes and coordinates are identical to 1.2.0. Persian names are identical too, except 25 that now use Persian letters ([item 4](#4-25-persian-names-now-use-persian-letters)). Old slugs and names that 1.2.0 already accepted (`gilan/sowme'eh-sara`, `kermanshah/هلشی`, `حلاشی`) still resolve.
41
+ - **Object shapes:** `Province`, `City`, `FlatCity`, `ProvinceListItem` and `Coordinates` keep every field they had. A few fields were added ([item 3](#3-new-required-fields-on-the-data-types)).
42
+ - **Arrays that were fresh in 1.x are still fresh and mutable:** `getProvincesList()`, `getCapitals()`, `searchCities()` and `searchProvinces()` return a new array on every call, which you can sort or change freely, exactly as in 1.x.
43
+ - **Arrays that were shared in 1.x keep the same identity:** `getProvinces()`, `getAllCities()`, `getCities(...)` and `getCitiesByName(...)` return the same array on every call, as in 1.x, so they stay safe to use in React dependency arrays.
44
+ - **React hooks:** same names, same return fields (`province`, `city`, `setProvince`, `setCity`, `provinceId`, `cityId`, `selectedProvince`, `selectedCity`, `reset`, `query`, `setQuery`, `results`). `setQuery` still accepts an updater function.
45
+
46
+ ## Breaking changes
47
+
48
+ ### 1. Returned data is frozen and array types are `readonly`
49
+
50
+ **What changed.** In 1.x the package handed out its internal arrays and objects. Changing them (for example `getCities('fars').sort(...)`) silently corrupted the data for every other part of your application, including other requests on the same server. In 2.0 all provinces, cities and coordinates are frozen with `Object.freeze`, and the shared arrays are typed as `readonly`.
51
+
52
+ Affected values:
53
+
54
+ | Value | 1.x type | 2.0 type |
55
+ |---|---|---|
56
+ | `getProvinces()` | `Province[]` | `readonly Province[]` |
57
+ | `getCities(province)` | `City[]` | `readonly City[]` |
58
+ | `getAllCities()` | `FlatCity[]` | `readonly FlatCity[]` |
59
+ | `getCitiesByName(name)` | `FlatCity[]` | `readonly FlatCity[]` |
60
+ | `Province.cities` | `City[]` | `readonly City[]` |
61
+ | `useCities(province)` | `City[]` | `readonly City[]` |
62
+ | Each `Province`, `City`, `FlatCity`, `Coordinates` object | mutable | frozen, properties typed `readonly` |
63
+
64
+ Not affected: `getProvincesList()`, `getCapitals()`, `searchCities()`, `searchProvinces()`, `useProvinces()` and the search hooks still return fresh, mutable arrays. The objects returned by `getProvincesList()` and `useProvinces()` are fresh as well and can be modified.
65
+
66
+ **How it shows up.**
67
+
68
+ - TypeScript: `error TS4104: The type 'readonly City[]' is 'readonly' and cannot be assigned to the mutable type 'City[]'`, or `Cannot assign to 'fa' because it is a read-only property`.
69
+ - At runtime, in ES modules, TypeScript output and any `'use strict'` code (this includes Next.js, Vite and webpack builds): `TypeError: Cannot assign to read only property ...` or `Cannot add property ..., object is not extensible`.
70
+ - At runtime, in non-strict CommonJS: the change is silently ignored.
71
+
72
+ **How to fix.**
73
+
74
+ Change the type annotation if you only read the value:
75
+
76
+ ```ts
77
+ // 1.x
78
+ const cities: City[] = getCities('fars');
79
+ // 2.0
80
+ const cities: readonly City[] = getCities('fars');
81
+ ```
82
+
83
+ Copy the array before sorting or changing it:
84
+
85
+ ```ts
86
+ // 1.x (also changed the shared data for everyone)
87
+ const sorted = getCities('fars').sort((a, b) => a.fa.localeCompare(b.fa, 'fa'));
88
+ // 2.0
89
+ const sorted = [...getCities('fars')].sort((a, b) => a.fa.localeCompare(b.fa, 'fa'));
90
+ ```
91
+
92
+ Copy the objects before changing their fields:
93
+
94
+ ```ts
95
+ // 1.x
96
+ const cities = getCities('fars');
97
+ cities.forEach((c) => { (c as any).label = c.fa; });
98
+ // 2.0
99
+ const cities = getCities('fars').map((c) => ({ ...c, label: c.fa }));
100
+ ```
101
+
102
+ If you pass the value to an API that expects a mutable array, copy it at the call site: `someFn([...getAllCities()])`.
103
+
104
+ If a third-party library mutates the objects you give it (some table and select components do), pass a deep copy: `structuredClone(getProvinces())` (Node.js 17+ and all current browsers) or `JSON.parse(JSON.stringify(getProvinces()))`.
105
+
106
+ ### 2. `getCity(name)` picks a different city for 7 shared names
107
+
108
+ **What changed.** 25 city names exist in more than one province. When you look up such a name **without** a province, 1.x returned the one with the lowest id, which was often a small town. 2.0 returns:
109
+
110
+ 1. the provincial capital, if one of them is a capital; otherwise
111
+ 2. the city marked as the well-known one in the data; otherwise
112
+ 3. the one with the lowest id (same as 1.x).
113
+
114
+ This affects `getCity(name)` and `getProvinceOfCity(name)` only. Lookups by id, by slug, or with a province (`getCity(name, province)`) are unchanged. `getCitiesByName(name)` is unchanged and still returns every match, ordered by id.
115
+
116
+ The complete list of lookups whose result changed (Persian and English names behave the same):
117
+
118
+ | Name | 1.x result | 2.0 result |
119
+ |---|---|---|
120
+ | `سردشت` / `sardasht` | Chaharmahal and Bakhtiari (id 104) | West Azerbaijan (id 1179) |
121
+ | `شهریار` / `shahriar` | East Azerbaijan (id 184) | Tehran (id 1132) |
122
+ | `مهاباد` / `mahabad` | Isfahan (id 270) | West Azerbaijan (id 1171) |
123
+ | `رضوانشهر` / `rezvanshahr` | Isfahan (id 279) | Gilan (id 408) |
124
+ | `حاجی آباد` / `hajjiabad` | Fars (id 312) | Hormozgan (id 512) |
125
+ | `اردکان` / `ardakan` | Fars (id 322) | Yazd (id 1212) |
126
+ | `چناران` / `chenaran` | North Khorasan (id 928) | Razavi Khorasan (id 975) |
127
+
128
+ **Who is affected.** Only code that calls `getCity()` or `getProvinceOfCity()` with one of these names and **without** a province, and depends on which city comes back, for example code that converts free-text input to an id and stores it.
129
+
130
+ **How to fix.** In order of preference:
131
+
132
+ ```ts
133
+ // Best: pass the province whenever you have it (result is unambiguous, same in 1.x and 2.0)
134
+ getCity('مهاباد', 'isfahan');
135
+
136
+ // Or store and look up ids instead of names
137
+ getCityById(270);
138
+
139
+ // Or detect ambiguous names and let the user choose
140
+ if (isAmbiguousCityName(input)) {
141
+ const options = getCitiesByName(input);
142
+ }
143
+
144
+ // Exact 1.x behaviour (lowest id wins)
145
+ const city = getCitiesByName(name)[0] ?? getCity(name);
146
+ ```
147
+
148
+ If you stored ids that were produced by name-only lookups of these 7 names with 1.x, they still point to the same cities as before; nothing changes for stored data. Only new lookups return the other city.
149
+
150
+ ### 3. New required fields on the data types
151
+
152
+ **What changed.** These fields were added and are always present:
153
+
154
+ | Type | New fields |
155
+ |---|---|
156
+ | `City` | `nameEn` (English display name, e.g. `'Bandar-e Gaz'`), `aliases` (other spellings and old names) |
157
+ | `FlatCity` | the two above, plus `provinceNameEn` |
158
+ | `Province` | `nameEn`, `neighborIds` |
159
+ | `ProvinceListItem` | `nameEn` |
160
+
161
+ **Who is affected.** Only TypeScript code that **creates** these objects by hand, typically test fixtures and mocks:
162
+
163
+ ```
164
+ error TS2741: Property 'nameEn' is missing in type '{ ... }' but required in type 'City'.
165
+ ```
166
+
167
+ **How to fix.** Add the fields:
168
+
169
+ ```ts
170
+ const mockCity: City = { id: 1, isCapital: true, slug: 'alborz/karaj', en: 'karaj', fa: 'کرج', nameEn: 'Karaj', aliases: [] };
171
+ ```
172
+
173
+ Or build fixtures from real data, which always stays in sync with the types: `const mockCity = getCityById(1)!;`
174
+
175
+ ### 4. 25 Persian names now use Persian letters
176
+
177
+ **What changed.** In 1.x, 25 city names were written with the Arabic letters «ي» and «ك» instead of the Persian «ی» and «ک», for example `كدكن` and `اسفرورين`. They looked wrong in lists and sorted in the wrong place. In 2.0 they use Persian letters: `کدکن`, `اسفرورین`. Ids, English ids and slugs did not change.
178
+
179
+ **Who is affected.** Only code that compares `city.fa` **as a plain string** with a value saved from 1.x, for example a database column of Persian names. Lookups are not affected: `getCity('كدكن')` still finds the city, because every lookup normalizes both letters.
180
+
181
+ **How to fix.** Compare ids instead of names, or compare normalized text:
182
+
183
+ ```ts
184
+ import { normalize, getDataChanges } from '@code-plate/iran-cities';
185
+
186
+ normalize(savedName) === normalize(city.fa);
187
+
188
+ for (const change of getDataChanges('1.2.0')) {
189
+ if (change.type === 'spelling') {
190
+ await db.query('UPDATE addresses SET city_fa = $1 WHERE city_id = $2', [change.after?.fa, change.cityId]);
191
+ }
192
+ }
193
+ ```
194
+
195
+ `getDataChanges('1.2.0')` lists all 25 changes with the old and new spelling.
196
+
197
+ ## Behaviour changes that are not breaking
198
+
199
+ These changes can alter search results, mostly by finding a city where 1.x found nothing, by reordering equally ranked results, or by dropping weak fuzzy matches.
200
+
201
+ | Area | 1.x | 2.0 |
202
+ |---|---|---|
203
+ | Normalization | `ي→ی`, `ك→ک`, ignores spaces, ZWNJ and `-` | Also `ى/ئ→ی`, `آ/أ/إ/ٱ→ا`, `ة/ۀ→ه`, `ؤ→و`; ignores diacritics, tatweel `ـ`, `_` and apostrophes; reads Persian and Arabic digits. `getCity('ابادان')` and `getProvince('۲۸')` now work. |
204
+ | Aliases | Not supported | `getCity('طهران')`, `getCity('esfahan')`, `getCity('mashad')`, `getCity('orumiyeh')`, `getCity('teheran')`, `getCity('urumieh')` now return a city instead of `undefined`. |
205
+ | Area codes | `'021'`, `'21'`, `21` | Also `'۰۲۱'`, `'+9821'`, `'+98 21'` and `'0098 21'`. |
206
+ | Search ranking | Equal scores kept data order | Equal scores: provincial capitals first, then Persian alphabetical order. Because of the wider normalization and the new aliases, a query can also match a few more cities than before. |
207
+ | Fuzzy search (`fuzzy: true`) | Allowed 2 typos for any query, so short queries matched unrelated names (`تهرن` also returned `اهر`, `بهمن`) | Allowed typos are capped at a third of the query length (`تهرن` returns only `تهران`), and typos at the start of longer names are matched too. |
208
+ | Wrong keyboard layout | `searchCities('jivhk')` returned nothing | Returns `تهران`: when nothing matches, the query is retried as if typed on the other layout. Turn off with `{ keyboardFix: false }`. |
209
+ | Invalid input | `getCity(undefined)`, `searchCities(null)` and similar threw `TypeError` | Return `undefined`, an empty array or all items, as for an empty string. |
210
+ | `limit` option | A negative `limit` dropped items from the end | A negative or zero `limit` means no limit. |
211
+ | `useProvinceCitySelect` | Kept the selected English names in state | Keeps ids in state. Returned values are the same. A city that does not belong to the selected province is now ignored. |
212
+ | `@code-plate/iran-cities/react` | Contained its own copy of all data (about 238 KB unminified) | Reuses the main entry. Apps that import both no longer ship the data twice, and both share one instance. |
213
+ | `react` and `lite/react` | No directive | Start with `"use client"`, so Next.js App Router treats them as client modules. |
214
+
215
+ ## New features you can adopt
216
+
217
+ - `@code-plate/iran-cities/lite` and `@code-plate/iran-cities/lite/react`: the same API without coordinates, about a third smaller. Ideal for province/city pickers.
218
+ - Accessible components with a plain-CSS theme: `CityCombobox` and `ProvinceCitySelect` from `@code-plate/iran-cities/lite/react/components`, styled by `@code-plate/iran-cities/components.css`.
219
+ - Validation schemas for Zod (`/zod`) and Valibot (`/valibot`), including a check that the city belongs to the province.
220
+ - Neighboring provinces: `getNeighborProvinces(query)`, `areNeighborProvinces(a, b)` and `Province.neighborIds`.
221
+ - Province boundaries as GeoJSON and `getProvinceAt({ lat, lng })` from `@code-plate/iran-cities/boundaries`.
222
+ - English display names: `nameEn` on provinces and cities, `provinceNameEn` on flat cities.
223
+ - `convertKeyboardLayout(text, 'fa' | 'en')`, `DATA_VERSION`, `DATA_CHANGES` and `getDataChanges(sinceVersion)`.
224
+ - CSV, SQL, JSON and GeoJSON copies of the data in `@code-plate/iran-cities/data/*`.
225
+ - `getDistance(from, to)` and `getNearestCities(point, { limit, minConfidence, maxDistanceKm })`.
226
+ - `isAmbiguousCityName(name)` and `normalize(text)`.
227
+ - `searchCities(query, { emptyQuery: 'none' })` returns `[]` for an empty query instead of all 1252 cities.
228
+ - `useProvinceCitySelect({ initialProvince, initialCity })` for edit forms, and `useProvinceCitySelect({ value, onChange })` for controlled use with react-hook-form, Formik and similar libraries.
229
+ - New exported types: `IranCities`, `IranCitiesGeo`, `IranCitiesHooks`, `LatLng`, `CoordinateConfidence`, `NearestCity`, `NearestCitiesOptions`, `Stats`, `ProvinceCityValue`, `ProvinceCitySelectOptions`, `ProvinceCitySelect`, `SearchState`, `CityComboboxProps`, `ProvinceCitySelectProps`, `DataChange`, `DataChangeType`, `KeyboardTarget`.
230
+
231
+ ## Next.js
232
+
233
+ Nothing has to change in a Next.js project. Recommended improvements:
234
+
235
+ ```tsx
236
+ // app/page.tsx (Server Component): use the core entry directly
237
+ import { getCity, getProvincesList } from '@code-plate/iran-cities';
238
+
239
+ // app/address-form.tsx (Client Component): use the lite hooks
240
+ 'use client';
241
+ import { useProvinceCitySelect } from '@code-plate/iran-cities/lite/react';
242
+ ```
243
+
244
+ - The hook entries already contain `"use client"`. You still need `'use client'` in your own component file that calls the hooks, as with any hook.
245
+ - Use `lite/react` in client components unless the browser needs coordinates. This keeps about 8 KB (brotli) out of the client bundle.
246
+ - Data returned on the server can be passed to Client Components as props; frozen objects serialize normally.
247
+ - Verified with Next.js 14 (webpack, React 18), Next.js 15 (webpack) and Next.js 16 (Turbopack and webpack, React 19), App Router, static rendering and `next start`.
248
+
249
+ ## Finding affected code
250
+
251
+ ```bash
252
+ # Values that may be mutated (check each hit)
253
+ grep -rnE "get(Provinces|AllCities|Cities|CitiesByName)\([^)]*\)\.(sort|push|pop|shift|unshift|splice|reverse|fill|copyWithin)\(" src
254
+
255
+ # Name-only lookups of the 7 changed names
256
+ grep -rnE "get(City|ProvinceOfCity)\(['\"](سردشت|sardasht|شهریار|shahriar|مهاباد|mahabad|رضوانشهر|rezvanshahr|حاجی ?آباد|hajjiabad|اردکان|ardakan|چناران|chenaran)['\"]\)" src
257
+
258
+ # Hand-written City objects
259
+ grep -rnE ":\s*(Flat)?City\s*=\s*\{" src
260
+ ```
261
+
262
+ Running your test suite in strict mode (any ESM or TypeScript test runner) turns accidental mutations into `TypeError`s, which makes them easy to find.
263
+
264
+ ## FAQ
265
+
266
+ **Can I keep using 1.x?** Yes. 1.x keeps working, but it will not get data fixes or new features.
267
+
268
+ **Why freeze the data instead of returning copies everywhere?** The functions that return shared arrays did so in 1.x too, and many apps rely on their identity being stable (for example in `useMemo` or `useEffect` dependencies). Copying on every call would have changed that and caused extra renders. Freezing keeps identity and makes accidental mutation visible instead of silently corrupting data. Functions that returned fresh arrays in 1.x still do.
269
+
270
+ **Did ids or slugs change?** No. Stored ids and slugs from 1.x stay valid.
271
+
272
+ **Does 2.0 need a newer Node.js, React or TypeScript?** No. The minimums are the same as in 1.x: Node.js 14, React 16.8, and TypeScript 3.9 for the type declarations.
package/NOTICE.md ADDED
@@ -0,0 +1,38 @@
1
+ # Attribution / منابع و مجوز داده‌ها
2
+
3
+ Coordinates (`lat`/`lng`) in `src/data.json` are point locations from **GeoNames** (https://www.geonames.org),
4
+ licensed under **Creative Commons Attribution 4.0** (CC BY 4.0). Attribution to GeoNames is required.
5
+
6
+ - Source file: GeoNames Iran dump `IR.zip` (https://download.geonames.org/export/dump/IR.zip), supplied by the package author on 2026-10-07.
7
+ - The 31 provincial-capital coordinates were supplied by the package author (Wikipedia table), cross-checked against GeoNames;
8
+ two were corrected (Ahvaz, Kermanshah).
9
+ - Each city was matched to a GeoNames populated place by normalized Persian name within the same province
10
+ (province mapped through GeoNames admin1 codes), with a Latin-name fallback. Abandoned/destroyed/historical places were excluded.
11
+
12
+ The following public datasets were used **only to cross-check the match** (no values were copied from them unless they equal the GeoNames value):
13
+
14
+ | Dataset | Used for | License |
15
+ |---|---|---|
16
+ | sajaddp/list-of-cities-in-Iran | cross-check; official county seats (GeoNames-derived) | repo: GPL-3.0; coordinate records: CC BY 4.0 (GeoNames) |
17
+ | dr5hn/countries-states-cities-database | cross-check only | ODbL-1.0 |
18
+ | jafaripur/iran-locality-database | cross-check only | none stated |
19
+ | mhndev/iran-geography | cross-check only | none stated |
20
+
21
+ This is a summary, not legal advice.
22
+
23
+ ## Province boundaries and neighbors
24
+
25
+ `src/boundaries.json` (published as `@code-plate/iran-cities/boundaries` and `data/iran-provinces.geojson`) is derived from
26
+ **geoBoundaries** `IRN-ADM1` (https://www.geoboundaries.org), which is based on **OpenStreetMap** data
27
+ (© OpenStreetMap contributors, https://www.openstreetmap.org/copyright).
28
+
29
+ - License of this boundary data: **Open Database License 1.0** (ODbL-1.0, https://opendatacommons.org/licenses/odbl/1-0/).
30
+ If you redistribute the boundaries, or a database derived from them, you must keep this attribution and offer it under ODbL.
31
+ The rest of the package is not affected: the code is MIT and the coordinates are CC BY 4.0.
32
+ - Changes made: provinces were merged into one feature each; a polygon around Bandar-e Gaz that the source labels
33
+ Mazandaran was reassigned to Golestan (Bandar-e Gaz county has belonged to Golestan since 1997); geometry was
34
+ simplified with a shared-edge (coverage) simplification at a 0.01° tolerance and rounded to 4 decimals.
35
+ The script is `scripts/geo/build-boundaries.py`.
36
+ - The `neighbors` of each province are computed from these boundaries (a shared border of at least 3 km). They were
37
+ cross-checked against Natural Earth (public domain), which predates the 2013 move of Tabas county from Yazd to
38
+ South Khorasan; geoBoundaries reflects the current borders.