thai-address-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,539 @@
1
+ # thai-address-sdk
2
+
3
+ ไลบรารี TypeScript/JavaScript สำหรับค้นหา กรอง ตรวจคำสะกด และจัดรูปแบบข้อมูล
4
+ จังหวัด อำเภอ/เขต และตำบล/แขวงของประเทศไทยแบบ offline
5
+
6
+ ข้อมูลและระบบค้นหาถูก bundle อยู่ใน package จึงไม่ต้องเรียก REST API, ไม่ต้องใช้ API
7
+ key และไม่ส่งข้อความที่อยู่ของผู้ใช้ออกจาก application
8
+
9
+ ## ความสามารถ
10
+
11
+ - ข้อมูลครบ 77 จังหวัด พร้อมอำเภอ/เขตและตำบล/แขวง
12
+ - ค้นหาด้วยชื่อไทย ชื่ออังกฤษ และ alias ที่ใช้ทั่วไป
13
+ - รองรับ exact, prefix, contains และ fuzzy search
14
+ - ช่วยแก้คำสะกด เช่น `อยูทยา` → `พระนครศรีอยุธยา`
15
+ - ตรวจความสัมพันธ์จังหวัด → อำเภอ → ตำบลจากข้อความหลายส่วน
16
+ - กรองข้อมูลสำหรับทำ dependent dropdown
17
+ - จัดรูปแบบที่อยู่ไทยเต็ม ไทยย่อ ไม่มีคำนำหน้า และภาษาอังกฤษ
18
+ - รองรับ ESM, CommonJS และมี TypeScript declarations
19
+ - ทำงานได้ทั้ง Node.js และ browser ผ่าน bundler
20
+
21
+ ## ขอบเขตของ package
22
+
23
+ Package นี้จัดการเฉพาะข้อมูลต่อไปนี้:
24
+
25
+ ```text
26
+ จังหวัด → อำเภอ/เขต → ตำบล/แขวง → รหัสไปรษณีย์
27
+ ```
28
+
29
+ ยังไม่แยกเลขที่บ้าน หมู่บ้าน อาคาร ซอย ถนน พิกัด GPS หรือข้อมูลส่วนบุคคลอื่น
30
+
31
+ ## การติดตั้ง
32
+
33
+ เมื่อนำ package ขึ้น npm registry แล้ว:
34
+
35
+ ```bash
36
+ npm install thai-address-sdk
37
+ ```
38
+
39
+ หรือใช้ package manager อื่น:
40
+
41
+ ```bash
42
+ pnpm add thai-address-sdk
43
+ yarn add thai-address-sdk
44
+ ```
45
+
46
+ ระหว่างพัฒนาใน repository นี้ สามารถติดตั้งจากโฟลเดอร์โดยตรง:
47
+
48
+ ```bash
49
+ npm install ../thai-address/npm-sdk
50
+ ```
51
+
52
+ ต้องใช้ Node.js 18 ขึ้นไปสำหรับการพัฒนาและทดสอบ package
53
+
54
+ ## เริ่มต้นใช้งาน
55
+
56
+ ```ts
57
+ import {
58
+ getDistricts,
59
+ getProvinces,
60
+ getSubdistricts,
61
+ normalizeAddress,
62
+ search,
63
+ } from "thai-address-sdk";
64
+
65
+ // ค้นหาชื่อที่สะกดผิด
66
+ const results = search("อยูทยา");
67
+
68
+ // วิเคราะห์หลายส่วนและคืน hierarchy ที่สัมพันธ์กัน
69
+ const normalized = normalizeAddress("บางปะอิน อยูทยา");
70
+
71
+ // ใช้สร้าง dropdown จังหวัด → อำเภอ → ตำบล
72
+ const provinces = getProvinces();
73
+ const districts = getDistricts({ provinceCode: 14 });
74
+ const subdistricts = getSubdistricts({ districtCode: 1406 });
75
+ ```
76
+
77
+ ทุกฟังก์ชันเป็น synchronous เพราะประมวลผลจากข้อมูลใน memory:
78
+
79
+ ```ts
80
+ const results = search("เชียงใหม่"); // ไม่ต้อง await
81
+ ```
82
+
83
+ ## การ Import
84
+
85
+ ### ESM และ TypeScript
86
+
87
+ ```ts
88
+ import { search, normalizeAddress } from "thai-address-sdk";
89
+ ```
90
+
91
+ ### CommonJS
92
+
93
+ ```js
94
+ const { search, normalizeAddress } = require("thai-address-sdk");
95
+ ```
96
+
97
+ ## `search(query, options?)`
98
+
99
+ ใช้ค้นหาหน่วยการปกครองจากคำเดียว โดยค้นหาได้ทั้งจังหวัด อำเภอ และตำบล
100
+
101
+ ```ts
102
+ const results = search("อยูทยา", {
103
+ levels: ["province"],
104
+ limit: 5,
105
+ minScore: 0.72,
106
+ format: "full_th",
107
+ });
108
+ ```
109
+
110
+ ### Search options
111
+
112
+ | Option | Type | Default | ความหมาย |
113
+ | ---------- | ----------------------------------------------- | ----------- | ------------------------------------- |
114
+ | `levels` | `("province" \| "district" \| "subdistrict")[]` | ทุกระดับ | จำกัดประเภทผลลัพธ์ |
115
+ | `limit` | `number` | `10` | จำนวนผลลัพธ์ สูงสุดภายในระบบคือ `100` |
116
+ | `minScore` | `number` | `0.72` | คะแนนต่ำสุดที่ยอมรับ |
117
+ | `format` | `"full_th" \| "short_th" \| "plain_th" \| "en"` | `"full_th"` | รูปแบบ `formattedAddress` |
118
+
119
+ ### ตัวอย่าง fuzzy search
120
+
121
+ ```ts
122
+ const [result] = search("อยูทยา", {
123
+ levels: ["province"],
124
+ limit: 1,
125
+ });
126
+
127
+ console.log(result);
128
+ ```
129
+
130
+ ผลลัพธ์:
131
+
132
+ ```json
133
+ {
134
+ "type": "province",
135
+ "province": {
136
+ "id": 5,
137
+ "provinceCode": 14,
138
+ "provinceNameEn": "Phra Nakhon Si Ayutthaya",
139
+ "provinceNameTh": "พระนครศรีอยุธยา"
140
+ },
141
+ "formattedAddress": "จังหวัดพระนครศรีอยุธยา",
142
+ "match": {
143
+ "input": "อยูทยา",
144
+ "matchedText": "อยุธยา",
145
+ "matchType": "fuzzy",
146
+ "confidence": 0.817,
147
+ "corrected": true
148
+ }
149
+ }
150
+ ```
151
+
152
+ ### ประเภทการ match
153
+
154
+ | `matchType` | ความหมาย |
155
+ | ----------- | ------------------------------------------ |
156
+ | `exact` | ตรงกับชื่อใน dataset ทุกตัวอักษร |
157
+ | `alias` | ตรงกับชื่อย่อหรือชื่อที่ใช้ทั่วไป |
158
+ | `prefix` | คำค้นหรือชื่อข้อมูลขึ้นต้นตรงกัน |
159
+ | `contains` | คำค้นเป็นส่วนหนึ่งของชื่อ หรือในทางกลับกัน |
160
+ | `fuzzy` | สะกดใกล้เคียงจากการคำนวณระยะห่างของคำ |
161
+
162
+ `confidence` อยู่ระหว่าง `0` ถึง `1` และเป็นคะแนนจัดอันดับภายใน SDK
163
+ ไม่ควรนำไปตีความเป็นเปอร์เซ็นต์ความถูกต้องทางสถิติ
164
+
165
+ ถ้าต้องการให้ผลลัพธ์เข้มงวดขึ้น สามารถเพิ่ม `minScore`:
166
+
167
+ ```ts
168
+ const strictResults = search("อยูทยา", { minScore: 0.9 });
169
+ ```
170
+
171
+ ## `normalizeAddress(input, options?)`
172
+
173
+ ใช้เมื่อตัว input มีหลายส่วน เช่นจังหวัดร่วมกับอำเภอหรือตำบล ระบบจะใช้ hierarchy
174
+ ช่วยเลือกผลลัพธ์ที่สัมพันธ์กัน
175
+
176
+ ```ts
177
+ const result = normalizeAddress("บางปะอิน อยูทยา");
178
+ ```
179
+
180
+ ผลลัพธ์:
181
+
182
+ ```json
183
+ {
184
+ "input": "บางปะอิน อยูทยา",
185
+ "normalizedInput": "บางปะอิน อยูทยา",
186
+ "status": "matched",
187
+ "bestMatch": {
188
+ "province": {
189
+ "provinceCode": 14,
190
+ "provinceNameTh": "พระนครศรีอยุธยา",
191
+ "provinceNameEn": "Phra Nakhon Si Ayutthaya"
192
+ },
193
+ "district": {
194
+ "districtCode": 1406,
195
+ "districtNameTh": "บางปะอิน",
196
+ "districtNameEn": "Bang Pa-In"
197
+ },
198
+ "postalCode": 13160,
199
+ "formattedAddress": "อำเภอบางปะอิน จังหวัดพระนครศรีอยุธยา"
200
+ },
201
+ "confidence": 0.929,
202
+ "alternatives": [],
203
+ "corrections": [
204
+ {
205
+ "input": "อยูทยา",
206
+ "normalized": "พระนครศรีอยุธยา",
207
+ "field": "province",
208
+ "matchType": "fuzzy"
209
+ }
210
+ ]
211
+ }
212
+ ```
213
+
214
+ > ตัวอย่างด้านบนตัด field `id`, code เชื่อมโยง และ `postalCode` บางส่วนออกเพื่อให้อ่านง่าย
215
+ > object ที่ได้จริงจะมี field ครบตาม TypeScript types
216
+
217
+ ### Normalize options
218
+
219
+ ```ts
220
+ const result = normalizeAddress("ต สุเทพ อ เมือง จ เชียงใหม่", {
221
+ format: "short_th",
222
+ limit: 5,
223
+ minScore: 0.72,
224
+ });
225
+ ```
226
+
227
+ | Option | Default | ความหมาย |
228
+ | ---------- | ----------- | ---------------------------------------------- |
229
+ | `format` | `"full_th"` | รูปแบบที่อยู่ผลลัพธ์ |
230
+ | `limit` | `5` | จำนวน candidate ที่ใช้พิจารณา สูงสุด `20` |
231
+ | `minScore` | `0.72` | คะแนนขั้นต่ำของแต่ละส่วนที่นำมาสร้าง hierarchy |
232
+
233
+ ### Normalize status
234
+
235
+ | Status | ความหมาย |
236
+ | ----------- | ------------------------------------------------------ |
237
+ | `matched` | พบผลลัพธ์ที่ผ่านเกณฑ์และไม่มีคู่แข่งคะแนนใกล้เคียง |
238
+ | `partial` | พบข้อมูลบางส่วนแต่คะแนนยังไม่สูงพอ |
239
+ | `ambiguous` | มีมากกว่าหนึ่งผลลัพธ์ที่คะแนนใกล้กัน ควรให้ผู้ใช้เลือก |
240
+ | `not_found` | ไม่พบผลลัพธ์ที่ผ่าน `minScore` |
241
+
242
+ ควรตรวจ `status` และ `bestMatch` ทุกครั้งก่อนนำข้อมูลไปบันทึก:
243
+
244
+ ```ts
245
+ const result = normalizeAddress(userInput);
246
+
247
+ switch (result.status) {
248
+ case "matched":
249
+ saveAddress(result.bestMatch);
250
+ break;
251
+ case "ambiguous":
252
+ showAddressChoices([result.bestMatch, ...result.alternatives]);
253
+ break;
254
+ case "partial":
255
+ askForMoreAddressDetails(result.bestMatch);
256
+ break;
257
+ case "not_found":
258
+ showNotFoundMessage();
259
+ break;
260
+ }
261
+ ```
262
+
263
+ ## `smartSearch(input, options?)`
264
+
265
+ `smartSearch` เป็น alias ของ `normalizeAddress` และคืนผลลัพธ์รูปแบบเดียวกัน:
266
+
267
+ ```ts
268
+ import { smartSearch } from "thai-address-sdk";
269
+
270
+ const result = smartSearch("ต สุเทพ อ เมือง จ เชียงใหม่");
271
+ console.log(result.bestMatch?.formattedAddress);
272
+ // ตำบลสุเทพ อำเภอเมืองเชียงใหม่ จังหวัดเชียงใหม่
273
+ ```
274
+
275
+ เลือกใช้ชื่อที่เหมาะกับบริบทของ application ได้ แต่ไม่จำเป็นต้องเรียกทั้งสองฟังก์ชัน
276
+
277
+ ## Filter API สำหรับ dropdown
278
+
279
+ ### จังหวัดทั้งหมด
280
+
281
+ ```ts
282
+ import { getProvinces } from "thai-address-sdk";
283
+
284
+ const provinces = getProvinces();
285
+ ```
286
+
287
+ ### อำเภอทั้งหมดในจังหวัด
288
+
289
+ ```ts
290
+ import { getDistricts } from "thai-address-sdk";
291
+
292
+ const districts = getDistricts({ provinceCode: 14 });
293
+ ```
294
+
295
+ ### ตำบลทั้งหมดในอำเภอ
296
+
297
+ ```ts
298
+ import { getSubdistricts } from "thai-address-sdk";
299
+
300
+ const subdistricts = getSubdistricts({ districtCode: 1406 });
301
+ ```
302
+
303
+ สามารถกรองตำบลด้วยทั้งจังหวัดและอำเภอพร้อมกัน:
304
+
305
+ ```ts
306
+ const subdistricts = getSubdistricts({
307
+ provinceCode: 14,
308
+ districtCode: 1406,
309
+ });
310
+ ```
311
+
312
+ ### ค้นหาด้วย code
313
+
314
+ ```ts
315
+ import { getDistrict, getProvince, getSubdistrict } from "thai-address-sdk";
316
+
317
+ const province = getProvince(14);
318
+ const district = getDistrict(1406);
319
+ const subdistrict = getSubdistrict(140601);
320
+ ```
321
+
322
+ ฟังก์ชันแบบ code คืน `undefined` เมื่อไม่พบข้อมูล:
323
+
324
+ ```ts
325
+ const province = getProvince(999);
326
+
327
+ if (!province) {
328
+ console.log("ไม่พบจังหวัด");
329
+ }
330
+ ```
331
+
332
+ ## ตัวอย่าง dependent dropdown
333
+
334
+ ```ts
335
+ import { getDistricts, getProvinces, getSubdistricts } from "thai-address-sdk";
336
+
337
+ const state = {
338
+ provinceCode: undefined as number | undefined,
339
+ districtCode: undefined as number | undefined,
340
+ };
341
+
342
+ const provinceOptions = getProvinces().map((province) => ({
343
+ value: province.provinceCode,
344
+ label: province.provinceNameTh,
345
+ }));
346
+
347
+ function onProvinceChange(provinceCode: number) {
348
+ state.provinceCode = provinceCode;
349
+ state.districtCode = undefined;
350
+
351
+ return getDistricts({ provinceCode }).map((district) => ({
352
+ value: district.districtCode,
353
+ label: district.districtNameTh,
354
+ }));
355
+ }
356
+
357
+ function onDistrictChange(districtCode: number) {
358
+ state.districtCode = districtCode;
359
+
360
+ return getSubdistricts({ districtCode }).map((subdistrict) => ({
361
+ value: subdistrict.subdistrictCode,
362
+ label: subdistrict.subdistrictNameTh,
363
+ postalCode: subdistrict.postalCode,
364
+ }));
365
+ }
366
+ ```
367
+
368
+ เมื่อเปลี่ยนจังหวัดควร reset อำเภอและตำบลที่เลือกไว้ เพื่อไม่ให้เกิด hierarchy ที่ไม่สัมพันธ์กัน
369
+
370
+ ## `formatAddress(address, format?)`
371
+
372
+ ```ts
373
+ import {
374
+ formatAddress,
375
+ getDistrict,
376
+ getProvince,
377
+ getSubdistrict,
378
+ } from "thai-address-sdk";
379
+
380
+ const province = getProvince(10);
381
+ const district = getDistrict(1047);
382
+ const subdistrict = getSubdistrict(104702);
383
+
384
+ if (province && district && subdistrict) {
385
+ formatAddress({ province, district, subdistrict }, "full_th");
386
+ // แขวงบางนาเหนือ เขตบางนา กรุงเทพมหานคร
387
+
388
+ formatAddress({ province, district, subdistrict }, "plain_th");
389
+ // บางนาเหนือ บางนา กรุงเทพมหานคร
390
+
391
+ formatAddress({ province, district, subdistrict }, "en");
392
+ // Bang Na Nuea, Bang Na, Bangkok
393
+ }
394
+ ```
395
+
396
+ รูปแบบที่รองรับ:
397
+
398
+ | Format | ตัวอย่าง |
399
+ | ---------- | ------------------------------------------------ |
400
+ | `full_th` | `ตำบลสุเทพ อำเภอเมืองเชียงใหม่ จังหวัดเชียงใหม่` |
401
+ | `short_th` | `ต.สุเทพ อ.เมืองเชียงใหม่ จ.เชียงใหม่` |
402
+ | `plain_th` | `สุเทพ เมืองเชียงใหม่ เชียงใหม่` |
403
+ | `en` | `Suthep, Mueang Chiang Mai, Chiang Mai` |
404
+
405
+ กรุงเทพมหานครจะใช้ `แขวง` และ `เขต` อัตโนมัติ
406
+
407
+ ## Text utilities
408
+
409
+ ฟังก์ชันเหล่านี้เป็น utility ระดับต่ำสำหรับกรณีที่ application ต้องการจัดการข้อความเอง:
410
+
411
+ ```ts
412
+ import {
413
+ damerauLevenshtein,
414
+ normalizeText,
415
+ stripAddressLabels,
416
+ } from "thai-address-sdk";
417
+
418
+ normalizeText(" กรุงเทพฯ ");
419
+ // กรุงเทพ
420
+
421
+ stripAddressLabels("ต. สุเทพ อ. เมืองเชียงใหม่ จ. เชียงใหม่");
422
+ // สุเทพ เมืองเชียงใหม่ เชียงใหม่
423
+
424
+ damerauLevenshtein("อยูทยา", "อยุธยา");
425
+ // ระยะห่างของตัวอักษร
426
+ ```
427
+
428
+ สำหรับ use case ทั่วไปควรใช้ `search` หรือ `normalizeAddress` แทน utility เหล่านี้
429
+
430
+ ## TypeScript types
431
+
432
+ Package export types หลักดังนี้:
433
+
434
+ ```ts
435
+ import type {
436
+ AddressFormat,
437
+ AddressHierarchy,
438
+ AddressLevel,
439
+ District,
440
+ NormalizeResult,
441
+ Province,
442
+ SearchOptions,
443
+ SearchResult,
444
+ Subdistrict,
445
+ } from "thai-address-sdk";
446
+ ```
447
+
448
+ Code ทุกชนิดเป็น `number`:
449
+
450
+ ```ts
451
+ province.provinceCode; // 2 หลัก เช่น 14
452
+ district.districtCode; // 4 หลัก เช่น 1406
453
+ subdistrict.subdistrictCode; // 6 หลัก เช่น 140601
454
+ subdistrict.postalCode; // 5 หลัก เช่น 13160
455
+ ```
456
+
457
+ ควรใช้ code เป็น identifier และใช้ชื่อสำหรับแสดงผล เนื่องจากชื่อพื้นที่อาจซ้ำกันได้
458
+
459
+ ## ตัวอย่าง autocomplete ใน browser
460
+
461
+ ควร debounce การค้นหาเพื่อไม่ให้ fuzzy search ทำงานทุกครั้งที่ผู้ใช้กดปุ่ม:
462
+
463
+ ```ts
464
+ import { search } from "thai-address-sdk";
465
+
466
+ let timer: ReturnType<typeof setTimeout>;
467
+
468
+ searchInput.addEventListener("input", (event) => {
469
+ clearTimeout(timer);
470
+
471
+ timer = setTimeout(() => {
472
+ const query = (event.target as HTMLInputElement).value;
473
+ const results = query.length >= 2 ? search(query, { limit: 8 }) : [];
474
+ renderSuggestions(results);
475
+ }, 200);
476
+ });
477
+ ```
478
+
479
+ เนื่องจาก package bundle dataset มาด้วย JavaScript bundle ที่ build แล้วมีขนาดประมาณ 1.8 MB
480
+ ก่อน compression ควรใช้ dynamic import หากหน้าเว็บไม่ได้ใช้ข้อมูลที่อยู่ทันที:
481
+
482
+ ```ts
483
+ const thaiAddress = await import("thai-address-sdk");
484
+ const results = thaiAddress.search("เชียงใหม่");
485
+ ```
486
+
487
+ ## แนวทางจัดการ fuzzy result
488
+
489
+ - อย่าบันทึกผลลัพธ์ fuzzy โดยไม่ให้ผู้ใช้ตรวจสอบในงานที่ต้องการความแม่นยำสูง
490
+ - ถ้า `status` เป็น `ambiguous` ให้แสดง `alternatives` เพื่อให้ผู้ใช้เลือก
491
+ - ใช้จังหวัดหรืออำเภอที่ผู้ใช้เลือกไว้ช่วยจำกัดบริบท
492
+ - เพิ่ม `minScore` เมื่อต้องการลด false positive
493
+ - ใช้ debounce สำหรับช่อง autocomplete
494
+ - เก็บ code ของพื้นที่ ไม่ควรเก็บเฉพาะชื่อ
495
+
496
+ ## การพัฒนา package
497
+
498
+ ```bash
499
+ cd npm-sdk
500
+ npm install
501
+ npm run typecheck
502
+ npm test
503
+ npm run format:check
504
+ npm run build
505
+ ```
506
+
507
+ Scripts:
508
+
509
+ | Command | หน้าที่ |
510
+ | ---------------------- | ----------------------------------------- |
511
+ | `npm run build` | สร้าง ESM, CommonJS และ declaration files |
512
+ | `npm run typecheck` | ตรวจ TypeScript แบบ strict |
513
+ | `npm test` | Build และรัน integration tests |
514
+ | `npm run format` | จัดรูปแบบ source และเอกสารด้วย Prettier |
515
+ | `npm run format:check` | ตรวจรูปแบบโดยไม่แก้ไฟล์ |
516
+
517
+ ไฟล์ที่พร้อม publish จะอยู่ใน `dist/` สามารถตรวจ package ก่อน publish ได้ด้วย:
518
+
519
+ ```bash
520
+ npm pack --dry-run
521
+ ```
522
+
523
+ ## ข้อมูลและ License
524
+
525
+ ข้อมูลจังหวัด อำเภอ ตำบล และรหัสไปรษณีย์มาจาก
526
+ [`thailand-geography-data/thailand-geography-json`](https://github.com/thailand-geography-data/thailand-geography-json)
527
+ และถูก pin ไว้ที่ source commit ที่ระบุใน
528
+ [`THIRD_PARTY_LICENSES.md`](./THIRD_PARTY_LICENSES.md)
529
+
530
+ ก่อนอัปเดต dataset ควรตรวจจำนวนรายการ code ซ้ำ parent code ที่ไม่มีอยู่ และรัน test
531
+ ทั้งหมดอีกครั้ง
532
+
533
+ ## ข้อจำกัดปัจจุบัน
534
+
535
+ - Alias ที่มากับ SDK ยังเป็นชุดเริ่มต้น ไม่ครอบคลุมชื่อเรียกท้องถิ่นทั้งหมด
536
+ - Fuzzy threshold ยังต้องปรับจากคำค้นจริงเพิ่มเติม
537
+ - การค้นหาและ normalize เป็น synchronous และอาจใช้เวลามากขึ้นบนอุปกรณ์กำลังต่ำ
538
+ - Dataset เป็น snapshot และจะไม่อัปเดตจากอินเทอร์เน็ตอัตโนมัติ
539
+ - Package ยังไม่รองรับบ้านเลขที่ ถนน ซอย หมู่บ้าน และ geocoding
@@ -0,0 +1,27 @@
1
+ # Third-party data
2
+
3
+ The bundled Thailand geography dataset is sourced from
4
+ [`thailand-geography-data/thailand-geography-json`](https://github.com/thailand-geography-data/thailand-geography-json)
5
+ at commit `b8b3fb91c7df1129ff5b43cb46f7fcffadd2156b`.
6
+
7
+ MIT License
8
+
9
+ Copyright (c) 2023-Present Joe Takara
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.