@code-plate/iran-cities 1.2.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 +45 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.fa.md +277 -0
  4. package/MIGRATION.md +272 -0
  5. package/NOTICE.md +17 -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 -9307
  38. package/dist/index.d.cts +25 -0
  39. package/dist/index.d.mts +25 -25
  40. package/dist/index.mjs +3 -9264
  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 -9310
  74. package/dist/react.d.cts +7 -0
  75. package/dist/react.d.mts +7 -30
  76. package/dist/react.mjs +2 -9281
  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 +167 -22
  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-DwK3g-Z0.d.mts +0 -53
  130. package/dist/types-DwK3g-Z0.d.ts +0 -53
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ See [MIGRATION.md](./MIGRATION.md) ([فارسی](./MIGRATION.fa.md)) for upgrade steps. Supported Node.js (14+), React (16.8+) and TypeScript (3.9+) versions are unchanged.
6
+
7
+ ### Breaking
8
+ - Provinces, cities, coordinates and the shared arrays returned by `getProvinces()`, `getAllCities()`, `getCities()` and `getCitiesByName()` are frozen and typed as `readonly`. Functions that returned fresh arrays in 1.x still do, and identities that were stable in 1.x are still stable.
9
+ - `getCity(name)` / `getProvinceOfCity(name)` without a province prefer the provincial capital, then the better-known city, for names shared by several provinces. The result changed for 7 names: سردشت, شهریار, مهاباد, رضوانشهر, حاجی آباد, اردکان, چناران.
10
+ - New required fields: `City.nameEn`, `City.aliases`, `FlatCity.provinceNameEn`, `Province.nameEn`, `Province.neighborIds`, `ProvinceListItem.nameEn` (affects hand-written objects in TypeScript only).
11
+ - 25 Persian city names written with Arabic «ي»/«ك» now use Persian «ی»/«ک» (e.g. `كدكن` → `کدکن`). Lookups with either spelling still work; the changes are listed in `getDataChanges('1.2.0')`.
12
+
13
+ ### Added
14
+ - Neighboring provinces: `getNeighborProvinces()`, `areNeighborProvinces()`, `Province.neighborIds`.
15
+ - `@code-plate/iran-cities/boundaries`: province boundaries as GeoJSON, `getProvinceAt({ lat, lng })` and `getProvinceBoundary(id)`.
16
+ - Components: `CityCombobox` (WAI-ARIA combobox) and `ProvinceCitySelect` in `/react/components` and `/lite/react/components`, styled by the plain-CSS `@code-plate/iran-cities/components.css` (CSS variables, dark mode, RTL, `:where()` selectors).
17
+ - Validation: `@code-plate/iran-cities/zod` and `@code-plate/iran-cities/valibot` with `provinceIdSchema`, `cityIdSchema`, `provinceSchema`, `citySchema`, `provinceCitySchema` and custom messages.
18
+ - Search fixes text typed with the wrong keyboard layout (`jivhk` → تهران) and exports `convertKeyboardLayout()`; opt out with `keyboardFix: false`.
19
+ - English display names: `nameEn` (e.g. `Kohgiluyeh and Boyer-Ahmad`, `Bandar-e Gaz`) and `provinceNameEn`.
20
+ - `DATA_VERSION`, `DATA_CHANGES` and `getDataChanges(sinceVersion)`.
21
+ - `@code-plate/iran-cities/data/*`: CSV, JSON, SQL (PostgreSQL, MySQL, MariaDB, SQLite) and GeoJSON copies of the data.
22
+ - `@code-plate/iran-cities/lite` and `/lite/react`: same API without coordinates.
23
+ - `getDistance()`, `getNearestCities()`, `isAmbiguousCityName()`, `normalize()`, `SearchOptions.emptyQuery`.
24
+ - `useProvinceCitySelect({ initialProvince, initialCity, value, onChange })`.
25
+ - Documentation site with a live playground and an interactive map.
26
+
27
+ ### Changed
28
+ - Data is stored in a packed columnar format: the data inside the bundle is about 20% smaller (brotli) and parses faster.
29
+ - Normalization unifies «آ/أ/إ/ٱ», «ة/ۀ», «ؤ», «ى/ئ», strips diacritics, tatweel and apostrophes, and reads Persian/Arabic digits.
30
+ - Fuzzy search caps allowed typos at a third of the query length and also matches typos at the start of longer names.
31
+ - Search ties are broken by capital first, then Persian alphabetical order.
32
+ - Invalid input (`undefined`, `null`, `NaN`, negative `limit`) no longer throws.
33
+ - React entries share the core instead of bundling their own copy of the data, and start with `"use client"`.
34
+ - Build moved from tsup (esbuild) to tsdown (Rolldown + Oxc), TypeScript 7 and `isolatedDeclarations`; `exports` serves `.d.mts` / `.d.cts`, and a plain `.d.ts` set keeps TypeScript 3.9 to 4.6 working.
35
+ - Package license field is `MIT AND CC-BY-4.0 AND ODbL-1.0` to reflect the data sources.
36
+
37
+ ### Fixed
38
+ - 25 city names used Arabic letters (see Breaking).
39
+ - geoBoundaries labels Bandar-e Gaz county as Mazandaran; it is assigned to Golestan.
40
+ - README said only provincial capitals have coordinates; 1152 cities do.
41
+ - `allowScripts` in `package.json` is kept (it is npm's install-script allowlist); its stale `esbuild` entry was dropped because esbuild is no longer a dependency.
42
+
43
+ ### Tooling
44
+ - Vitest unit, component, accessibility (axe-core) and property-based (fast-check) tests with coverage thresholds.
45
+ - CI on Node.js 14–24, Deno, Bun, TypeScript 3.9–7 and Next.js 14–16; Oxlint, publint, Are The Types Wrong and size-limit budgets.
46
+ - OpenSSF Scorecard and Dependabot.
47
+
3
48
  ## 1.2.0
4
49
  - **Added:** numeric `id` for provinces and cities (globally unique, stable); `isCapital`, `capitalId`, province `phoneCode`.
5
50
  - **Added:** `coordinates: { lat, lng, confidence }` for 1152 cities (GeoNames, CC BY 4.0); `confidence` is `'high' | 'medium' | 'low'`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Javad Soleymani Fard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,277 @@
1
+ [English](./MIGRATION.md) | فارسی
2
+
3
+ # مهاجرت از نسخه‌ی 1.x به 2.0
4
+
5
+ نسخه‌ی 2.0 در بیشتر پروژه‌ها بدون هیچ تغییری جایگزین نسخه‌ی قبل می‌شود. کد جاوااسکریپت معمولی، بک‌اندهای Node.js/Express و فرم‌های رایج React/Next.js بدون تغییر کار می‌کنند. این راهنما **همه‌ی** تفاوت‌هایی را که ممکن است روی کد موجود اثر بگذارند فهرست می‌کند، توضیح می‌دهد چطور بفهمید پروژه‌تان درگیر است یا نه، و راه‌حل دقیق هر مورد را می‌گوید.
6
+
7
+ همه‌ی موارد با اجرای فراخوانی‌های یکسان روی build نسخه‌ی 1.2.0 و build نسخه‌ی 2.0.0 راستی‌آزمایی شده‌اند.
8
+
9
+ ## فهرست
10
+
11
+ - [چک‌لیست سریع](#چکلیست-سریع)
12
+ - [چه چیزهایی تغییر نکرده](#چه-چیزهایی-تغییر-نکرده)
13
+ - [تغییرات ناسازگار](#تغییرات-ناسازگار)
14
+ - [۱. داده‌های خروجی freeze شده‌اند و تایپ آرایه‌ها `readonly` است](#۱-دادههای-خروجی-freeze-شدهاند-و-تایپ-آرایهها-readonly-است)
15
+ - [۲. `getCity(name)` برای ۷ نام تکراری شهر دیگری برمی‌گرداند](#۲-getcityname-برای-۷-نام-تکراری-شهر-دیگری-برمیگرداند)
16
+ - [۳. فیلدهای اجباری جدید در تایپ‌های داده](#۳-فیلدهای-اجباری-جدید-در-تایپهای-داده)
17
+ - [۴. ۲۵ نام فارسی حالا با حروف فارسی نوشته می‌شوند](#۴-۲۵-نام-فارسی-حالا-با-حروف-فارسی-نوشته-میشوند)
18
+ - [تغییرات رفتاری غیرناسازگار](#تغییرات-رفتاری-غیرناسازگار)
19
+ - [قابلیت‌های جدید](#قابلیتهای-جدید)
20
+ - [Next.js](#nextjs)
21
+ - [پیدا کردن کدهای درگیر](#پیدا-کردن-کدهای-درگیر)
22
+ - [پرسش‌های رایج](#پرسشهای-رایج)
23
+
24
+ ## چک‌لیست سریع
25
+
26
+ ۱. `npm install @code-plate/iran-cities@^2`
27
+
28
+ ۲. کامپایلر TypeScript را اجرا کنید. خطاهایی به شکل `TS4104: The type 'readonly City[]' is 'readonly' and cannot be assigned to the mutable type 'City[]'` مربوط به [مورد ۱](#۱-دادههای-خروجی-freeze-شدهاند-و-تایپ-آرایهها-readonly-است) هستند و خطای `Property 'aliases' is missing` (یا `nameEn`، `neighborIds`، `provinceNameEn`) مربوط به [مورد ۳](#۳-فیلدهای-اجباری-جدید-در-تایپهای-داده).
29
+
30
+ ۳. در کدتان دنبال جاهایی بگردید که داده‌ی برگشتی از پکیج را **تغییر** می‌دهند (`.sort(`، `.push(`، `.splice(`، `.reverse(` یا مقداردهی مثل `city.xxx =`). بخش [پیدا کردن کدهای درگیر](#پیدا-کردن-کدهای-درگیر) را ببینید.
31
+
32
+ ۴. اگر `getCity()` یا `getProvinceOfCity()` را با نام شهر و بدون استان صدا می‌زنید، [۷ نام](#۲-getcityname-برای-۷-نام-تکراری-شهر-دیگری-برمیگرداند) را بررسی کنید.
33
+
34
+ ۵. اگر `city.fa` را با نام‌های فارسی ذخیره‌شده در دیتابیس مقایسه می‌کنید، [مورد ۴](#۴-۲۵-نام-فارسی-حالا-با-حروف-فارسی-نوشته-میشوند) را ببینید.
35
+
36
+ ۶. اختیاری: کامپوننت‌های کلاینت را به `@code-plate/iran-cities/lite/react` ببرید تا حجم bundle کلاینت حدود یک‌سوم کم شود.
37
+
38
+ ## چه چیزهایی تغییر نکرده
39
+
40
+ - **نسخه‌های پشتیبانی‌شده‌ی Node.js:** `engines.node` همچنان `>=14` است. فایل‌های منتشرشده روی Node.js نسخه‌های ۱۴، ۱۶، ۱۸، ۲۰، ۲۲ و ۲۴، هم به‌صورت CommonJS و هم ES module تست شده‌اند.
41
+ - **نسخه‌های پشتیبانی‌شده‌ی TypeScript:** تایپ‌ها از TypeScript 3.9 تا 7.x کار می‌کنند، با `moduleResolution` برابر `node`/`node10`، `node16`/`nodenext` یا `bundler`.
42
+ - **وابستگی React:** همچنان `>=16.8.0` و همچنان اختیاری است.
43
+ - **مسیرهای import:** `@code-plate/iran-cities` و `@code-plate/iran-cities/react` تغییری نکرده‌اند. دو ساب‌پس اضافه شده و هیچ‌کدام حذف نشده.
44
+ - **همه‌ی توابع و تایپ‌های نسخه‌ی 1.x** با همان نام و همان پارامترها وجود دارند.
45
+ - **idها، slugها و داده‌ها:** id شهرها و استان‌ها، شناسه‌های انگلیسی، slugها، کدهای تلفن و مختصات دقیقاً مثل 1.2.0 هستند. نام‌های فارسی هم همان‌اند، به‌جز ۲۵ نام که حالا با حروف فارسی نوشته می‌شوند ([مورد ۴](#۴-۲۵-نام-فارسی-حالا-با-حروف-فارسی-نوشته-میشوند)). slugها و نام‌های قدیمی‌ای که 1.2.0 قبول می‌کرد (`gilan/sowme'eh-sara`، `kermanshah/هلشی`، `حلاشی`) همچنان کار می‌کنند.
46
+ - **ساختار اشیا:** `Province`، `City`، `FlatCity`، `ProvinceListItem` و `Coordinates` همه‌ی فیلدهای قبلی را دارند و چند فیلد هم به آن‌ها اضافه شده ([مورد ۳](#۳-فیلدهای-اجباری-جدید-در-تایپهای-داده)).
47
+ - **آرایه‌هایی که در 1.x هر بار تازه ساخته می‌شدند، هنوز تازه و قابل‌تغییرند:** `getProvincesList()`، `getCapitals()`، `searchCities()` و `searchProvinces()` در هر فراخوانی آرایه‌ی جدیدی برمی‌گردانند که مثل 1.x می‌توانید sort یا تغییرش دهید.
48
+ - **آرایه‌هایی که در 1.x مشترک بودند، همان identity را دارند:** `getProvinces()`، `getAllCities()`، `getCities(...)` و `getCitiesByName(...)` مثل 1.x در هر فراخوانی همان آرایه را برمی‌گردانند و استفاده از آن‌ها در dependency arrayهای React همچنان امن است.
49
+ - **هوک‌های React:** همان نام‌ها و همان فیلدهای خروجی (`province`، `city`، `setProvince`، `setCity`، `provinceId`، `cityId`، `selectedProvince`، `selectedCity`، `reset`، `query`، `setQuery`، `results`). `setQuery` همچنان تابع updater هم قبول می‌کند.
50
+
51
+ ## تغییرات ناسازگار
52
+
53
+ ### ۱. داده‌های خروجی freeze شده‌اند و تایپ آرایه‌ها `readonly` است
54
+
55
+ **چه چیزی تغییر کرده.** در 1.x پکیج آرایه‌ها و اشیای داخلی خودش را مستقیم برمی‌گرداند. تغییر دادن آن‌ها (مثلاً `getCities('fars').sort(...)`) بی‌صدا داده‌ها را برای کل برنامه خراب می‌کرد، حتی برای درخواست‌های دیگر روی همان سرور. در 2.0 همه‌ی استان‌ها، شهرها و مختصات با `Object.freeze` قفل شده‌اند و آرایه‌های مشترک تایپ `readonly` دارند.
56
+
57
+ مقادیر درگیر:
58
+
59
+ | مقدار | تایپ در 1.x | تایپ در 2.0 |
60
+ |---|---|---|
61
+ | `getProvinces()` | `Province[]` | `readonly Province[]` |
62
+ | `getCities(province)` | `City[]` | `readonly City[]` |
63
+ | `getAllCities()` | `FlatCity[]` | `readonly FlatCity[]` |
64
+ | `getCitiesByName(name)` | `FlatCity[]` | `readonly FlatCity[]` |
65
+ | `Province.cities` | `City[]` | `readonly City[]` |
66
+ | `useCities(province)` | `City[]` | `readonly City[]` |
67
+ | هر شیء `Province`، `City`، `FlatCity` و `Coordinates` | قابل‌تغییر | freeze شده، فیلدها با تایپ `readonly` |
68
+
69
+ درگیر نیستند: `getProvincesList()`، `getCapitals()`، `searchCities()`، `searchProvinces()`، `useProvinces()` و هوک‌های جستجو همچنان آرایه‌ی تازه و قابل‌تغییر برمی‌گردانند. اشیای برگشتی از `getProvincesList()` و `useProvinces()` هم تازه‌اند و قابل تغییر.
70
+
71
+ **چطور خودش را نشان می‌دهد.**
72
+
73
+ - در TypeScript: خطای `error TS4104: The type 'readonly City[]' is 'readonly' and cannot be assigned to the mutable type 'City[]'` یا `Cannot assign to 'fa' because it is a read-only property`.
74
+ - در زمان اجرا، در ES moduleها، خروجی TypeScript و هر کد `'use strict'` (که شامل buildهای Next.js، Vite و webpack هم می‌شود): خطای `TypeError: Cannot assign to read only property ...` یا `Cannot add property ..., object is not extensible`.
75
+ - در زمان اجرا، در CommonJS غیر strict: تغییر بی‌صدا نادیده گرفته می‌شود.
76
+
77
+ **راه‌حل.**
78
+
79
+ اگر فقط مقدار را می‌خوانید، تایپ را عوض کنید:
80
+
81
+ ```ts
82
+ // 1.x
83
+ const cities: City[] = getCities('fars');
84
+ // 2.0
85
+ const cities: readonly City[] = getCities('fars');
86
+ ```
87
+
88
+ قبل از sort یا تغییر، از آرایه کپی بگیرید:
89
+
90
+ ```ts
91
+ // 1.x (داده‌ی مشترک را برای همه تغییر می‌داد)
92
+ const sorted = getCities('fars').sort((a, b) => a.fa.localeCompare(b.fa, 'fa'));
93
+ // 2.0
94
+ const sorted = [...getCities('fars')].sort((a, b) => a.fa.localeCompare(b.fa, 'fa'));
95
+ ```
96
+
97
+ قبل از تغییر فیلدها، از اشیا کپی بگیرید:
98
+
99
+ ```ts
100
+ // 1.x
101
+ const cities = getCities('fars');
102
+ cities.forEach((c) => { (c as any).label = c.fa; });
103
+ // 2.0
104
+ const cities = getCities('fars').map((c) => ({ ...c, label: c.fa }));
105
+ ```
106
+
107
+ اگر مقدار را به APIای می‌دهید که آرایه‌ی قابل‌تغییر می‌خواهد، همان‌جا کپی کنید: `someFn([...getAllCities()])`.
108
+
109
+ اگر یک کتابخانه‌ی دیگر اشیایی را که به آن می‌دهید تغییر می‌دهد (بعضی کامپوننت‌های جدول و select این کار را می‌کنند)، کپی عمیق بدهید: `structuredClone(getProvinces())` (Node.js 17 به بالا و همه‌ی مرورگرهای فعلی) یا `JSON.parse(JSON.stringify(getProvinces()))`.
110
+
111
+ ### ۲. `getCity(name)` برای ۷ نام تکراری شهر دیگری برمی‌گرداند
112
+
113
+ **چه چیزی تغییر کرده.** ۲۵ نام شهر در بیش از یک استان وجود دارد. وقتی چنین نامی را **بدون** استان جستجو می‌کردید، 1.x شهری را برمی‌گرداند که کوچک‌ترین id را داشت و اغلب یک شهر کوچک بود. 2.0 به این ترتیب انتخاب می‌کند:
114
+
115
+ ۱. اگر یکی از آن‌ها مرکز استان باشد، همان؛ وگرنه
116
+ ۲. شهری که در داده‌ها به‌عنوان شهر شناخته‌شده‌تر علامت خورده؛ وگرنه
117
+ ۳. شهری که کوچک‌ترین id را دارد (مثل 1.x).
118
+
119
+ این تغییر فقط روی `getCity(name)` و `getProvinceOfCity(name)` اثر دارد. جستجو با id، با slug یا همراه با استان (`getCity(name, province)`) تغییری نکرده است. `getCitiesByName(name)` هم تغییری نکرده و همچنان همه‌ی شهرهای هم‌نام را به ترتیب id برمی‌گرداند.
120
+
121
+ فهرست کامل جستجوهایی که نتیجه‌شان عوض شده (نام فارسی و انگلیسی رفتار یکسانی دارند):
122
+
123
+ | نام | نتیجه در 1.x | نتیجه در 2.0 |
124
+ |---|---|---|
125
+ | `سردشت` / `sardasht` | چهارمحال و بختیاری (id 104) | آذربایجان غربی (id 1179) |
126
+ | `شهریار` / `shahriar` | آذربایجان شرقی (id 184) | تهران (id 1132) |
127
+ | `مهاباد` / `mahabad` | اصفهان (id 270) | آذربایجان غربی (id 1171) |
128
+ | `رضوانشهر` / `rezvanshahr` | اصفهان (id 279) | گیلان (id 408) |
129
+ | `حاجی آباد` / `hajjiabad` | فارس (id 312) | هرمزگان (id 512) |
130
+ | `اردکان` / `ardakan` | فارس (id 322) | یزد (id 1212) |
131
+ | `چناران` / `chenaran` | خراسان شمالی (id 928) | خراسان رضوی (id 975) |
132
+
133
+ **چه کسی درگیر است.** فقط کدی که `getCity()` یا `getProvinceOfCity()` را با یکی از این نام‌ها و **بدون** استان صدا می‌زند و به اینکه کدام شهر برمی‌گردد وابسته است؛ مثلاً کدی که ورودی متنی کاربر را به id تبدیل و ذخیره می‌کند.
134
+
135
+ **راه‌حل.** به ترتیب اولویت:
136
+
137
+ ```ts
138
+ // بهترین: هر وقت استان را دارید، آن را هم بدهید (نتیجه یکتاست و در 1.x و 2.0 یکسان است)
139
+ getCity('مهاباد', 'isfahan');
140
+
141
+ // یا به‌جای نام، id ذخیره و جستجو کنید
142
+ getCityById(270);
143
+
144
+ // یا نام‌های مبهم را تشخیص دهید و انتخاب را به کاربر بسپارید
145
+ if (isAmbiguousCityName(input)) {
146
+ const options = getCitiesByName(input);
147
+ }
148
+
149
+ // دقیقاً رفتار 1.x (کوچک‌ترین id)
150
+ const city = getCitiesByName(name)[0] ?? getCity(name);
151
+ ```
152
+
153
+ اگر idهایی را که از جستجوی همین ۷ نام بدون استان در 1.x به دست آمده ذخیره کرده‌اید، همچنان به همان شهرهای قبلی اشاره می‌کنند و داده‌های ذخیره‌شده تغییری نمی‌کنند. فقط جستجوهای جدید شهر دیگری برمی‌گردانند.
154
+
155
+ ### ۳. فیلدهای اجباری جدید در تایپ‌های داده
156
+
157
+ **چه چیزی تغییر کرده.** این فیلدها اضافه شده‌اند و همیشه مقدار دارند:
158
+
159
+ | تایپ | فیلدهای جدید |
160
+ |---|---|
161
+ | `City` | `nameEn` (نام نمایشی انگلیسی، مثلاً `'Bandar-e Gaz'`)، `aliases` (املاهای دیگر و نام‌های قدیمی) |
162
+ | `FlatCity` | دو فیلد بالا به‌علاوه‌ی `provinceNameEn` |
163
+ | `Province` | `nameEn`، `neighborIds` |
164
+ | `ProvinceListItem` | `nameEn` |
165
+
166
+ **چه کسی درگیر است.** فقط کد TypeScriptای که این اشیا را **دستی می‌سازد**؛ معمولاً fixtureها و mockهای تست:
167
+
168
+ ```
169
+ error TS2741: Property 'nameEn' is missing in type '{ ... }' but required in type 'City'.
170
+ ```
171
+
172
+ **راه‌حل.** فیلدها را اضافه کنید:
173
+
174
+ ```ts
175
+ const mockCity: City = { id: 1, isCapital: true, slug: 'alborz/karaj', en: 'karaj', fa: 'کرج', nameEn: 'Karaj', aliases: [] };
176
+ ```
177
+
178
+ یا fixture را از داده‌ی واقعی بسازید تا همیشه با تایپ‌ها هماهنگ بماند: `const mockCity = getCityById(1)!;`
179
+
180
+ ### ۴. ۲۵ نام فارسی حالا با حروف فارسی نوشته می‌شوند
181
+
182
+ **چه چیزی تغییر کرده.** در 1.x، نام ۲۵ شهر با حروف عربی «ي» و «ك» به‌جای «ی» و «ک» فارسی نوشته شده بود، مثل `كدكن` و `اسفرورين`. این نام‌ها در فهرست‌ها درست نمایش داده نمی‌شدند و جای اشتباهی مرتب می‌شدند. در 2.0 با حروف فارسی نوشته می‌شوند: `کدکن`، `اسفرورین`. idها، شناسه‌های انگلیسی و slugها تغییری نکرده‌اند.
183
+
184
+ **چه کسی درگیر است.** فقط کدی که `city.fa` را **به‌صورت رشته‌ی خام** با مقداری که از 1.x ذخیره شده مقایسه می‌کند؛ مثلاً ستونی از نام‌های فارسی در دیتابیس. جستجوها درگیر نیستند: `getCity('كدكن')` همچنان شهر را پیدا می‌کند، چون همه‌ی lookupها هر دو حرف را نرمال می‌کنند.
185
+
186
+ **راه‌حل.** به‌جای نام، id را مقایسه کنید، یا متن نرمال‌شده را:
187
+
188
+ ```ts
189
+ import { normalize, getDataChanges } from '@code-plate/iran-cities';
190
+
191
+ normalize(savedName) === normalize(city.fa);
192
+
193
+ for (const change of getDataChanges('1.2.0')) {
194
+ if (change.type === 'spelling') {
195
+ await db.query('UPDATE addresses SET city_fa = $1 WHERE city_id = $2', [change.after?.fa, change.cityId]);
196
+ }
197
+ }
198
+ ```
199
+
200
+ `getDataChanges('1.2.0')` هر ۲۵ تغییر را همراه با املای قبلی و جدید برمی‌گرداند.
201
+
202
+ ## تغییرات رفتاری غیرناسازگار
203
+
204
+ این تغییرات ممکن است نتیجه‌ی جستجو را عوض کنند؛ بیشتر به این شکل که جایی که 1.x چیزی پیدا نمی‌کرد حالا شهر پیدا می‌شود، ترتیب نتایج هم‌رتبه عوض می‌شود، یا نتیجه‌های فازی ضعیف حذف می‌شوند.
205
+
206
+ | بخش | 1.x | 2.0 |
207
+ |---|---|---|
208
+ | نرمال‌سازی | `ي→ی`، `ك→ک`؛ فاصله، نیم‌فاصله و `-` نادیده گرفته می‌شد | علاوه بر آن `ى/ئ→ی`، `آ/أ/إ/ٱ→ا`، `ة/ۀ→ه`، `ؤ→و`؛ اعراب، کشیده‌ی `ـ`، `_` و آپاستروف نادیده گرفته می‌شوند؛ ارقام فارسی و عربی خوانده می‌شوند. حالا `getCity('ابادان')` و `getProvince('۲۸')` کار می‌کنند. |
209
+ | نام‌های جایگزین | پشتیبانی نمی‌شد | `getCity('طهران')`، `getCity('esfahan')`، `getCity('mashad')`، `getCity('orumiyeh')`، `getCity('teheran')` و `getCity('urumieh')` به‌جای `undefined` شهر برمی‌گردانند. |
210
+ | کد تلفن | `'021'`، `'21'`، `21` | همچنین `'۰۲۱'`، `'+9821'`، `'+98 21'` و `'0098 21'`. |
211
+ | ترتیب جستجو | در امتیاز برابر، ترتیب داده‌ها حفظ می‌شد | در امتیاز برابر، اول مراکز استان و بعد ترتیب الفبای فارسی. به خاطر نرمال‌سازی گسترده‌تر و نام‌های جایگزین، یک جستجو ممکن است چند شهر بیشتر از قبل هم پیدا کند. |
212
+ | جستجوی فازی (`fuzzy: true`) | برای هر جستجویی ۲ غلط تایپی مجاز بود و جستجوهای کوتاه نام‌های بی‌ربط را هم برمی‌گرداندند (`تهرن` به `اهر` و `بهمن` هم می‌رسید) | تعداد غلط مجاز حداکثر یک‌سوم طول عبارت است (`تهرن` فقط `تهران` را برمی‌گرداند) و غلط در ابتدای نام‌های بلندتر هم پیدا می‌شود. |
213
+ | کیبورد اشتباه | `searchCities('jivhk')` چیزی برنمی‌گرداند | `تهران` را برمی‌گرداند: اگر نتیجه‌ای نباشد، عبارت طوری دوباره جستجو می‌شود که انگار با چیدمان دیگر تایپ شده. با `{ keyboardFix: false }` خاموش می‌شود. |
214
+ | ورودی نامعتبر | `getCity(undefined)`، `searchCities(null)` و موارد مشابه خطای `TypeError` می‌دادند | مثل رشته‌ی خالی رفتار می‌کنند: `undefined`، آرایه‌ی خالی یا همه‌ی موارد. |
215
+ | گزینه‌ی `limit` | `limit` منفی چند مورد از آخر را حذف می‌کرد | `limit` منفی یا صفر یعنی بدون محدودیت. |
216
+ | `useProvinceCitySelect` | نام انگلیسی انتخاب‌ها را در state نگه می‌داشت | id را در state نگه می‌دارد. خروجی‌ها همان قبلی‌اند. شهری که متعلق به استان انتخاب‌شده نباشد حالا نادیده گرفته می‌شود. |
217
+ | `@code-plate/iran-cities/react` | یک نسخه‌ی کامل جداگانه از داده‌ها داشت (حدود ۲۳۸ کیلوبایت بدون minify) | از ورودی اصلی استفاده می‌کند. برنامه‌ای که هر دو را import کند دیگر داده را دو بار ship نمی‌کند و هر دو یک نمونه‌ی مشترک دارند. |
218
+ | `react` و `lite/react` | directive نداشتند | با `"use client"` شروع می‌شوند تا App Router در Next.js آن‌ها را ماژول کلاینت حساب کند. |
219
+
220
+ ## قابلیت‌های جدید
221
+
222
+ - `@code-plate/iran-cities/lite` و `@code-plate/iran-cities/lite/react`: همان API بدون مختصات، حدود یک‌سوم سبک‌تر. مناسب برای انتخاب‌گر استان و شهر.
223
+ - کامپوننت‌های دسترس‌پذیر با تم CSS خام: `CityCombobox` و `ProvinceCitySelect` از `@code-plate/iran-cities/lite/react/components` با استایل `@code-plate/iran-cities/components.css`.
224
+ - اسکیمای اعتبارسنجی برای Zod (`/zod`) و Valibot (`/valibot`)، از جمله بررسی اینکه شهر متعلق به استان باشد.
225
+ - استان‌های همسایه: `getNeighborProvinces(query)`، `areNeighborProvinces(a, b)` و `Province.neighborIds`.
226
+ - مرز استان‌ها به‌صورت GeoJSON و `getProvinceAt({ lat, lng })` از `@code-plate/iran-cities/boundaries`.
227
+ - نام نمایشی انگلیسی: `nameEn` برای استان‌ها و شهرها و `provinceNameEn` برای شهرهای مسطح.
228
+ - `convertKeyboardLayout(text, 'fa' | 'en')`، `DATA_VERSION`، `DATA_CHANGES` و `getDataChanges(sinceVersion)`.
229
+ - نسخه‌های CSV، SQL، JSON و GeoJSON داده‌ها در `@code-plate/iran-cities/data/*`.
230
+ - `getDistance(from, to)` و `getNearestCities(point, { limit, minConfidence, maxDistanceKm })`.
231
+ - `isAmbiguousCityName(name)` و `normalize(text)`.
232
+ - `searchCities(query, { emptyQuery: 'none' })` برای query خالی به‌جای هر ۱۲۵۲ شهر، `[]` برمی‌گرداند.
233
+ - `useProvinceCitySelect({ initialProvince, initialCity })` برای فرم‌های ویرایش، و `useProvinceCitySelect({ value, onChange })` برای حالت controlled با react-hook-form، Formik و کتابخانه‌های مشابه.
234
+ - تایپ‌های جدید: `IranCities`، `IranCitiesGeo`، `IranCitiesHooks`، `LatLng`، `CoordinateConfidence`، `NearestCity`، `NearestCitiesOptions`، `Stats`، `ProvinceCityValue`، `ProvinceCitySelectOptions`، `ProvinceCitySelect`، `SearchState`، `CityComboboxProps`، `ProvinceCitySelectProps`، `DataChange`، `DataChangeType`، `KeyboardTarget`.
235
+
236
+ ## Next.js
237
+
238
+ در پروژه‌ی Next.js لازم نیست چیزی را تغییر دهید. بهبودهای پیشنهادی:
239
+
240
+ ```tsx
241
+ // app/page.tsx (Server Component): مستقیم از ورودی اصلی استفاده کنید
242
+ import { getCity, getProvincesList } from '@code-plate/iran-cities';
243
+
244
+ // app/address-form.tsx (Client Component): از هوک‌های lite استفاده کنید
245
+ 'use client';
246
+ import { useProvinceCitySelect } from '@code-plate/iran-cities/lite/react';
247
+ ```
248
+
249
+ - ورودی‌های هوک خودشان `"use client"` دارند. مثل هر هوک دیگری، فایل کامپوننتی که هوک را صدا می‌زند هم باید `'use client'` داشته باشد.
250
+ - در کامپوننت‌های کلاینت از `lite/react` استفاده کنید، مگر اینکه مرورگر مختصات لازم داشته باشد. این کار حدود ۸ کیلوبایت (brotli) از bundle کلاینت کم می‌کند.
251
+ - داده‌هایی که روی سرور می‌گیرید را می‌توانید به‌عنوان props به Client Componentها بدهید؛ اشیای freeze شده بدون مشکل serialize می‌شوند.
252
+ - با Next.js 14 (webpack و React 18)، Next.js 15 (webpack) و Next.js 16 (Turbopack و webpack و React 19)، App Router، رندر استاتیک و `next start` راستی‌آزمایی شده است.
253
+
254
+ ## پیدا کردن کدهای درگیر
255
+
256
+ ```bash
257
+ # مقادیری که ممکن است تغییر داده شوند (هر مورد را بررسی کنید)
258
+ grep -rnE "get(Provinces|AllCities|Cities|CitiesByName)\([^)]*\)\.(sort|push|pop|shift|unshift|splice|reverse|fill|copyWithin)\(" src
259
+
260
+ # جستجوی ۷ نامِ تغییرکرده بدون استان
261
+ grep -rnE "get(City|ProvinceOfCity)\(['\"](سردشت|sardasht|شهریار|shahriar|مهاباد|mahabad|رضوانشهر|rezvanshahr|حاجی ?آباد|hajjiabad|اردکان|ardakan|چناران|chenaran)['\"]\)" src
262
+
263
+ # اشیای City که دستی ساخته شده‌اند
264
+ grep -rnE ":\s*(Flat)?City\s*=\s*\{" src
265
+ ```
266
+
267
+ اجرای تست‌ها در حالت strict (هر test runner با ESM یا TypeScript) تغییرات ناخواسته را به `TypeError` تبدیل می‌کند و پیدا کردنشان را ساده می‌کند.
268
+
269
+ ## پرسش‌های رایج
270
+
271
+ **می‌توانم همچنان از 1.x استفاده کنم؟** بله. 1.x همچنان کار می‌کند، ولی اصلاحات داده و قابلیت‌های جدید را نمی‌گیرد.
272
+
273
+ **چرا داده‌ها freeze شده‌اند و به‌جایش همه‌جا کپی برنگردانده‌اید؟** توابعی که آرایه‌ی مشترک برمی‌گردانند در 1.x هم همین کار را می‌کردند و خیلی از برنامه‌ها به ثابت بودن identity آن‌ها وابسته‌اند (مثلاً در dependencyهای `useMemo` یا `useEffect`). کپی گرفتن در هر فراخوانی این رفتار را عوض می‌کرد و رندرهای اضافه ایجاد می‌کرد. freeze کردن identity را حفظ می‌کند و تغییر ناخواسته را به‌جای خراب کردن بی‌صدای داده‌ها، آشکار می‌کند. توابعی که در 1.x آرایه‌ی تازه برمی‌گرداندند، همچنان همین کار را می‌کنند.
274
+
275
+ **idها یا slugها عوض شده‌اند؟** نه. idها و slugهای ذخیره‌شده از 1.x همچنان معتبرند.
276
+
277
+ **نسخه‌ی 2.0 به Node.js، React یا TypeScript جدیدتری نیاز دارد؟** نه. حداقل‌ها همان 1.x است: Node.js 14، React 16.8 و TypeScript 3.9 برای تایپ‌ها.
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.