@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/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
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
+
48
+ ## 1.2.0
49
+ - **Added:** numeric `id` for provinces and cities (globally unique, stable); `isCapital`, `capitalId`, province `phoneCode`.
50
+ - **Added:** `coordinates: { lat, lng, confidence }` for 1152 cities (GeoNames, CC BY 4.0); `confidence` is `'high' | 'medium' | 'low'`.
51
+ - **Added:** `getCityById`, `getProvinceById`, `getCitiesByName`, `getCapital`, `getCapitals`, `getProvinceByPhoneCode`; lookups accept numeric ids.
52
+ - **Added:** `npm run validate` data-integrity checks.
53
+ - **Fixed:** duplicate «گلوگاه» entry in Mazandaran removed (1253 -> 1252 cities); `useCitySearch` / `useProvinceSearch` no longer re-run on inline `options`.
54
+ - **Corrected data (old names/slugs still resolve):** city 402 `sowme'eh-sara` -> `sowmeeh-sara` (fa: صومعه سرا); city 672 `هلشی` -> `halashi` (fa: هلشی). Ids unchanged.
55
+ - **Corrected coordinates:** Ahvaz and Kermanshah capital coordinates fixed (source table was ~110 km / ~60 km off).
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 برای تایپ‌ها.