thaizip 0.7.6 → 0.7.8
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/LICENSE +28 -0
- package/README.md +25 -146
- package/dist/data.cjs +11 -5
- package/dist/data.js +11 -5
- package/dist/defaultData.js +2 -2
- package/dist/index.cjs +12 -8
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +12 -8
- package/dist/react.cjs +1 -3
- package/dist/react.d.cts +1 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +1 -3
- package/package.json +10 -10
package/LICENSE
CHANGED
|
@@ -19,3 +19,31 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
The bundled address data (data/*.json, src/data/defaultData.ts) is derived
|
|
26
|
+
from https://github.com/kongvut/thai-province-data and is used under the
|
|
27
|
+
following license:
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2025 Kongvut Sangkla
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,177 +1,56 @@
|
|
|
1
1
|
# thaizip
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/thaizip)
|
|
4
|
-
[](https://www.npmjs.com/package/thaizip)
|
|
5
4
|
[](https://github.com/naay99999/thai-zip/blob/main/LICENSE)
|
|
6
5
|
|
|
7
|
-
Fast fuzzy autocomplete for Thai addresses — subdistrict, district, province, postal code.
|
|
6
|
+
Fast fuzzy autocomplete for Thai addresses — subdistrict, district, province, postal code.
|
|
8
7
|
|
|
9
|
-
**
|
|
8
|
+
📚 **[Documentation](https://naay99999.github.io/thai-zip/)** (ไทย / [English](https://naay99999.github.io/thai-zip/en/)) · [Playground](https://naay99999.github.io/thai-zip/en/playground/)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Fuzzy search** in Thai or English — typos and common spellings like `ladprao` still match
|
|
15
|
+
- **Postal code lookup** — exact or prefix (`10230`, `102`)
|
|
16
|
+
- **Cascade dropdowns** — province → district → subdistrict
|
|
17
|
+
- **Bundled data** — 77 provinces, 918 districts, 7,385 subdistricts, loaded lazily
|
|
18
|
+
- **Zero runtime dependencies**, works anywhere JS runs; optional React hook
|
|
19
|
+
|
|
20
|
+
## Install
|
|
14
21
|
|
|
15
22
|
```bash
|
|
16
23
|
npm install thaizip
|
|
17
24
|
```
|
|
18
25
|
|
|
19
|
-
| import path | contents |
|
|
20
|
-
|---|---|
|
|
21
|
-
| `thaizip` | core functions + types (no React code, 4.4 KB gzip) |
|
|
22
|
-
| `thaizip/react` | `useThaiAddressAutocomplete` (ships `"use client"`) |
|
|
23
|
-
| `thaizip/data` | `loadDefaultIndex`, `clearDefaultIndex`, `getDefaultIndexIfLoaded` (2.3 KB gzip, plus a 114 KB gzip data chunk fetched lazily on the first `loadDefaultIndex()` call) |
|
|
24
|
-
|
|
25
26
|
## Quick start
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
import { loadDefaultIndex } from 'thaizip/data'
|
|
29
|
-
import { searchThaiAddress,
|
|
30
|
+
import { searchThaiAddress, resolveThaiAddress } from 'thaizip'
|
|
30
31
|
|
|
31
|
-
const index = await loadDefaultIndex()
|
|
32
|
+
const index = await loadDefaultIndex()
|
|
32
33
|
|
|
33
|
-
searchThaiAddress(index, 'ลาดพร้าว')
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
formatThaiAddressSuggestion(record) // for the dropdown
|
|
38
|
-
formatThaiAddressSuggestion(record, { locale: 'en' })
|
|
39
|
-
resolveThaiAddress(record) // for saving
|
|
34
|
+
const [record] = searchThaiAddress(index, 'ลาดพร้าว')
|
|
35
|
+
resolveThaiAddress(record)
|
|
36
|
+
// { subdistrict: 'ลาดพร้าว', district: 'เขตลาดพร้าว', province: 'กรุงเทพมหานคร', postalCode: '10230', ... }
|
|
40
37
|
```
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
The cached default index is shared. Ordinary writes to its records and lookup structures throw; build a separate index with `buildThaiAddressIndex` if you need to edit address data. This is protection against accidental changes, not a security boundary against hostile code already running in the same JavaScript realm.
|
|
44
|
-
|
|
45
|
-
## Search
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
searchThaiAddress(index, query, {
|
|
49
|
-
limit: 10, // text queries only
|
|
50
|
-
threshold: 0.4, // match quality 0–1
|
|
51
|
-
zipLimit: Infinity, // all-digit queries — unlimited by default
|
|
52
|
-
romanizationAliases: true, // expand non-RTGS spellings
|
|
53
|
-
})
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Results rank by trigram score, then by how well the query matches the subdistrict's **own** name (exact → prefix → substring), then alphabetically. That second key is what keeps ตำบลลาดพร้าว above the other subdistricts that merely sit inside เขตลาดพร้าว.
|
|
57
|
-
|
|
58
|
-
> Combined text + zip in one query (`"ลาดพร้าว 10900"`) is not supported — search by name or zip separately.
|
|
59
|
-
|
|
60
|
-
### Postal codes
|
|
61
|
-
|
|
62
|
-
One Thai postal code can cover many subdistricts — `45000` covers 33, and 230 of the 953 codes cover more than 10 — so `zipLimit` defaults to unlimited rather than silently truncating at `limit`.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { lookupByZipCode } from 'thaizip'
|
|
66
|
-
|
|
67
|
-
lookupByZipCode(index, '45000') // exact
|
|
68
|
-
lookupByZipCode(index, '450') // prefix
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Direct ZIP lookup rejects raw inputs longer than 1000 characters before scanning them. A finite `zipLimit` also bounds how many matching records are collected.
|
|
72
|
-
|
|
73
|
-
### English input
|
|
74
|
-
|
|
75
|
-
The dataset indexes official RTGS spellings, so `bang rak` and `chatuchak` work directly. 87 common non-RTGS spellings are mapped on first: `lardprao` → `lat phrao`, `ladkrabang` → `lat krabang`, `krungthep` → `bangkok`. It is a curated dictionary, not a transliterator — unlisted spellings still miss. Opt out with `romanizationAliases: false`.
|
|
76
|
-
|
|
77
|
-
### Cascade dropdowns
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { listProvinces, listAmphures, listTambons } from 'thaizip'
|
|
81
|
-
|
|
82
|
-
listProvinces(index) // 77, sorted by Thai name
|
|
83
|
-
listAmphures(index, provinceId)
|
|
84
|
-
listTambons(index, amphureId) // each with its zipCode
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Backed by pre-built groupings, not scans over all 7,385 records. Unknown ids return `[]`.
|
|
88
|
-
|
|
89
|
-
## React
|
|
39
|
+
React:
|
|
90
40
|
|
|
91
41
|
```tsx
|
|
92
42
|
import { useThaiAddressAutocomplete } from 'thaizip/react'
|
|
93
43
|
|
|
94
|
-
const { query, setQuery,
|
|
95
|
-
useThaiAddressAutocomplete({ index })
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Options: `index`, `limit`, `debounce` (200ms), `threshold`, `zipLimit`, `locale`, `initialQuery`, `onSelect`.
|
|
99
|
-
|
|
100
|
-
| value | description |
|
|
101
|
-
|---|---|
|
|
102
|
-
| `query` / `setQuery` | input value; `setQuery` searches |
|
|
103
|
-
| `setQuerySilent` | set the value **without** searching or reopening the dropdown |
|
|
104
|
-
| `suggestions` | `ThaiAddressSuggestion[]` |
|
|
105
|
-
| `isOpen` | `query` is non-empty and suggestions exist |
|
|
106
|
-
| `selectSuggestion` | `(item) => ResolvedThaiAddress \| null` — clears suggestions, leaves `query` alone; `null` if the item is stale |
|
|
107
|
-
| `clear` | reset query and suggestions |
|
|
108
|
-
|
|
109
|
-
### Controlled forms
|
|
110
|
-
|
|
111
|
-
`selectSuggestion` leaves `query` untouched by design. To echo the choice back into the input use `setQuerySilent` — plain `setQuery` would immediately reopen the dropdown:
|
|
112
|
-
|
|
113
|
-
```tsx
|
|
114
|
-
const handlePick = (s: ThaiAddressSuggestion) => {
|
|
115
|
-
const address = selectSuggestion(s)
|
|
116
|
-
if (!address) return
|
|
117
|
-
setQuerySilent(s.label)
|
|
118
|
-
form.setValue('address', address)
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Editing a saved address? Pass `initialQuery` rather than calling `setQuery` in an effect.
|
|
123
|
-
|
|
124
|
-
### Accessibility
|
|
125
|
-
|
|
126
|
-
The hook owns query and suggestion state only. Keyboard navigation and ARIA combobox semantics are yours — `npx react-thaizip add autocomplete` scaffolds a component that already has them.
|
|
127
|
-
|
|
128
|
-
## Custom data
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
import { buildThaiAddressIndex, validateRawData } from 'thaizip'
|
|
132
|
-
|
|
133
|
-
const index = buildThaiAddressIndex({ provinces, amphures, tambons }, {
|
|
134
|
-
onSkip: (tambon) => console.warn('skipped:', tambon.id),
|
|
135
|
-
validate: true, // default
|
|
136
|
-
})
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Input is validated by default, so a stray non-string field fails with `[thaizip] tambon 100404: expected string for name_th, got number` instead of crashing inside the normalizer. `validateRawData(data)` runs the same checks standalone.
|
|
140
|
-
|
|
141
|
-
Validation also rejects more than 50,000 total rows, names longer than 256 UTF-16 code units, ZIP values longer than 32 code units, or an estimated trigram posting cost above 1,000,000. The estimate counts each province/district's unique trigrams once for every active child subdistrict, so it catches parent-name fan-out. These bounds are suitable for the bundled-size dataset, not a guarantee that arbitrary uploads are safe to process. For larger trusted datasets, pass `{ validate: false }` and apply your own resource limits before building.
|
|
142
|
-
|
|
143
|
-
Types: `RawData`, `RawProvince`, `RawAmphure`, `RawTambon`.
|
|
144
|
-
|
|
145
|
-
## Types
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
type ThaiAddressSuggestion = {
|
|
149
|
-
id: string
|
|
150
|
-
label: string // follows `locale`, Thai by default
|
|
151
|
-
labelTh: string
|
|
152
|
-
labelEn: string
|
|
153
|
-
tambon: string; tambonEn: string
|
|
154
|
-
amphure: string; amphureEn: string
|
|
155
|
-
province: string; provinceEn: string
|
|
156
|
-
zipCode: string
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
type ResolvedThaiAddress = {
|
|
160
|
-
tambon: string; tambonEn: string // alias: subdistrict / subdistrictEn
|
|
161
|
-
amphure: string; amphureEn: string // alias: district / districtEn
|
|
162
|
-
province: string; provinceEn: string
|
|
163
|
-
zipCode: string // alias: postalCode
|
|
164
|
-
subdistrict: string; subdistrictEn: string
|
|
165
|
-
district: string; districtEn: string
|
|
166
|
-
postalCode: string
|
|
167
|
-
}
|
|
44
|
+
const { query, setQuery, suggestions, selectSuggestion } = useThaiAddressAutocomplete({ index })
|
|
168
45
|
```
|
|
169
46
|
|
|
170
|
-
|
|
47
|
+
Need a ready-made UI component? [`react-thaizip`](https://github.com/naay99999/react-thai-zip) scaffolds one into your project.
|
|
171
48
|
|
|
172
|
-
|
|
49
|
+
## Learn more
|
|
173
50
|
|
|
174
|
-
|
|
51
|
+
- [Getting started](https://naay99999.github.io/thai-zip/en/getting-started/)
|
|
52
|
+
- [Guides](https://naay99999.github.io/thai-zip/en/guides/search/) — search, postal codes, cascades, English input, React, custom data
|
|
53
|
+
- [API reference](https://naay99999.github.io/thai-zip/en/reference/search/)
|
|
175
54
|
|
|
176
55
|
## License
|
|
177
56
|
|
package/dist/data.cjs
CHANGED
|
@@ -42,6 +42,12 @@ var MAX_POSTING_BUDGET = 1e6;
|
|
|
42
42
|
function typeErr(table, id, field, expected, value) {
|
|
43
43
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
44
44
|
}
|
|
45
|
+
function assertId(table, id, field, value) {
|
|
46
|
+
if (typeof value !== "number") throw typeErr(table, id, field, "number", value);
|
|
47
|
+
if (!Number.isSafeInteger(value)) {
|
|
48
|
+
throw new TypeError(`[thaizip] ${table} ${id}: expected safe integer for ${field}, got ${value}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
45
51
|
function typeName(value) {
|
|
46
52
|
return value === null ? "null" : typeof value;
|
|
47
53
|
}
|
|
@@ -68,7 +74,7 @@ function validateRawData(data) {
|
|
|
68
74
|
if (p === null || typeof p !== "object") {
|
|
69
75
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
70
76
|
}
|
|
71
|
-
|
|
77
|
+
assertId("province", p.id, "id", p.id);
|
|
72
78
|
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
73
79
|
provinceById.set(p.id, p);
|
|
74
80
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
@@ -82,12 +88,12 @@ function validateRawData(data) {
|
|
|
82
88
|
if (a === null || typeof a !== "object") {
|
|
83
89
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
84
90
|
}
|
|
85
|
-
|
|
91
|
+
assertId("amphure", a.id, "id", a.id);
|
|
86
92
|
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
87
93
|
amphureById.set(a.id, a);
|
|
88
94
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
89
95
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
90
|
-
|
|
96
|
+
assertId("amphure", a.id, "province_id", a.province_id);
|
|
91
97
|
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
92
98
|
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
93
99
|
}
|
|
@@ -100,7 +106,7 @@ function validateRawData(data) {
|
|
|
100
106
|
if (t === null || typeof t !== "object") {
|
|
101
107
|
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
102
108
|
}
|
|
103
|
-
|
|
109
|
+
assertId("tambon", t.id, "id", t.id);
|
|
104
110
|
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
105
111
|
seenTambonIds.add(t.id);
|
|
106
112
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
@@ -111,7 +117,7 @@ function validateRawData(data) {
|
|
|
111
117
|
if (zipType !== "string" && zipType !== "number") {
|
|
112
118
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
113
119
|
}
|
|
114
|
-
|
|
120
|
+
assertId("tambon", t.id, "amphure_id", t.amphure_id);
|
|
115
121
|
const zipLength = String(t.zip_code).length;
|
|
116
122
|
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
117
123
|
if (t.deleted_at) continue;
|
package/dist/data.js
CHANGED
|
@@ -40,6 +40,12 @@ var MAX_POSTING_BUDGET = 1e6;
|
|
|
40
40
|
function typeErr(table, id, field, expected, value) {
|
|
41
41
|
return new TypeError(`[thaizip] ${table} ${id}: expected ${expected} for ${field}, got ${typeof value}`);
|
|
42
42
|
}
|
|
43
|
+
function assertId(table, id, field, value) {
|
|
44
|
+
if (typeof value !== "number") throw typeErr(table, id, field, "number", value);
|
|
45
|
+
if (!Number.isSafeInteger(value)) {
|
|
46
|
+
throw new TypeError(`[thaizip] ${table} ${id}: expected safe integer for ${field}, got ${value}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
43
49
|
function typeName(value) {
|
|
44
50
|
return value === null ? "null" : typeof value;
|
|
45
51
|
}
|
|
@@ -66,7 +72,7 @@ function validateRawData(data) {
|
|
|
66
72
|
if (p === null || typeof p !== "object") {
|
|
67
73
|
throw new TypeError(`[thaizip] province[${i}]: expected object, got ${typeName(p)}`);
|
|
68
74
|
}
|
|
69
|
-
|
|
75
|
+
assertId("province", p.id, "id", p.id);
|
|
70
76
|
if (provinceById.has(p.id)) throw new TypeError(`[thaizip] duplicate province id: ${p.id}`);
|
|
71
77
|
provinceById.set(p.id, p);
|
|
72
78
|
if (typeof p.name_th !== "string") throw typeErr("province", p.id, "name_th", "string", p.name_th);
|
|
@@ -80,12 +86,12 @@ function validateRawData(data) {
|
|
|
80
86
|
if (a === null || typeof a !== "object") {
|
|
81
87
|
throw new TypeError(`[thaizip] amphure[${i}]: expected object, got ${typeName(a)}`);
|
|
82
88
|
}
|
|
83
|
-
|
|
89
|
+
assertId("amphure", a.id, "id", a.id);
|
|
84
90
|
if (amphureById.has(a.id)) throw new TypeError(`[thaizip] duplicate amphure id: ${a.id}`);
|
|
85
91
|
amphureById.set(a.id, a);
|
|
86
92
|
if (typeof a.name_th !== "string") throw typeErr("amphure", a.id, "name_th", "string", a.name_th);
|
|
87
93
|
if (typeof a.name_en !== "string") throw typeErr("amphure", a.id, "name_en", "string", a.name_en);
|
|
88
|
-
|
|
94
|
+
assertId("amphure", a.id, "province_id", a.province_id);
|
|
89
95
|
if (a.name_th.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_th too long`);
|
|
90
96
|
if (a.name_en.length > MAX_NAME_LENGTH) throw new RangeError(`[thaizip] amphure ${a.id}: name_en too long`);
|
|
91
97
|
}
|
|
@@ -98,7 +104,7 @@ function validateRawData(data) {
|
|
|
98
104
|
if (t === null || typeof t !== "object") {
|
|
99
105
|
throw new TypeError(`[thaizip] tambon[${i}]: expected object, got ${typeName(t)}`);
|
|
100
106
|
}
|
|
101
|
-
|
|
107
|
+
assertId("tambon", t.id, "id", t.id);
|
|
102
108
|
if (seenTambonIds.has(t.id)) throw new TypeError(`[thaizip] duplicate tambon id: ${t.id}`);
|
|
103
109
|
seenTambonIds.add(t.id);
|
|
104
110
|
if (typeof t.name_th !== "string") throw typeErr("tambon", t.id, "name_th", "string", t.name_th);
|
|
@@ -109,7 +115,7 @@ function validateRawData(data) {
|
|
|
109
115
|
if (zipType !== "string" && zipType !== "number") {
|
|
110
116
|
throw typeErr("tambon", t.id, "zip_code", "string or number", t.zip_code);
|
|
111
117
|
}
|
|
112
|
-
|
|
118
|
+
assertId("tambon", t.id, "amphure_id", t.amphure_id);
|
|
113
119
|
const zipLength = String(t.zip_code).length;
|
|
114
120
|
if (zipLength > MAX_ZIP_LENGTH) throw new RangeError(`[thaizip] tambon ${t.id}: zip_code too long`);
|
|
115
121
|
if (t.deleted_at) continue;
|