@nomideusz/svelte-search 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/CHANGELOG.md +18 -0
- package/README.md +295 -0
- package/dist/core/engine.d.ts +25 -0
- package/dist/core/engine.js +384 -0
- package/dist/core/geo.d.ts +24 -0
- package/dist/core/geo.js +54 -0
- package/dist/core/indexer.d.ts +60 -0
- package/dist/core/indexer.js +302 -0
- package/dist/core/normalize.d.ts +30 -0
- package/dist/core/normalize.js +128 -0
- package/dist/core/resolver.d.ts +41 -0
- package/dist/core/resolver.js +107 -0
- package/dist/core/track.d.ts +10 -0
- package/dist/core/track.js +40 -0
- package/dist/core/types.d.ts +206 -0
- package/dist/core/types.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/locales/pl.d.ts +11 -0
- package/dist/locales/pl.js +266 -0
- package/package.json +66 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** SQL dialect for query generation. */
|
|
2
|
+
export type SqlDialect = 'sqlite' | 'postgres';
|
|
3
|
+
export interface DatabaseClient {
|
|
4
|
+
execute(query: {
|
|
5
|
+
sql: string;
|
|
6
|
+
args: unknown[];
|
|
7
|
+
} | string): Promise<{
|
|
8
|
+
rows: Record<string, unknown>[];
|
|
9
|
+
lastInsertRowid?: bigint | number;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export interface SchemaAdapter<TResult extends SearchResult = SearchResult> {
|
|
13
|
+
/** Table names in your database */
|
|
14
|
+
tables: {
|
|
15
|
+
/** Main entities table (e.g. "schools", "tours", "listings") */
|
|
16
|
+
entities: string;
|
|
17
|
+
/** Trigram index table (e.g. "school_trigrams", "tour_trigrams") */
|
|
18
|
+
trigrams: string;
|
|
19
|
+
/** FTS5 virtual table (e.g. "schools_fts", "tours_fts") */
|
|
20
|
+
fts: string;
|
|
21
|
+
/** Synonyms table (e.g. "search_synonyms") */
|
|
22
|
+
synonyms: string;
|
|
23
|
+
};
|
|
24
|
+
/** Column names in the entities table */
|
|
25
|
+
columns: {
|
|
26
|
+
/** Primary key column */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Display name column */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Normalized name column (for matching) */
|
|
31
|
+
nameNormalized: string;
|
|
32
|
+
/** URL slug column */
|
|
33
|
+
slug: string;
|
|
34
|
+
/** Latitude column (null if no geo) */
|
|
35
|
+
lat: string | null;
|
|
36
|
+
/** Longitude column (null if no geo) */
|
|
37
|
+
lng: string | null;
|
|
38
|
+
/** Location slug column (e.g. "city_slug", "destination_slug") */
|
|
39
|
+
locationSlug: string | null;
|
|
40
|
+
/** Normalized categories column (e.g. "styles_n", "categories_n") */
|
|
41
|
+
categoriesNormalized: string | null;
|
|
42
|
+
/** Normalized location name (e.g. "city_n", "destination_n") */
|
|
43
|
+
locationNormalized: string | null;
|
|
44
|
+
/** Normalized district/area column (e.g. "district_n", "area_n") */
|
|
45
|
+
areaNormalized: string | null;
|
|
46
|
+
};
|
|
47
|
+
/** Trigram table columns */
|
|
48
|
+
trigramColumns: {
|
|
49
|
+
/** Trigram value column */
|
|
50
|
+
trigram: string;
|
|
51
|
+
/** Foreign key to entity */
|
|
52
|
+
entityId: string;
|
|
53
|
+
/** Field name column (e.g. "name", "city", "style") */
|
|
54
|
+
field: string;
|
|
55
|
+
};
|
|
56
|
+
/** Convert a raw DB row to a typed SearchResult */
|
|
57
|
+
toResult(row: Record<string, unknown>, lat?: number, lng?: number): TResult;
|
|
58
|
+
/** Fields to extract trigrams from when indexing an entity */
|
|
59
|
+
trigramFields(entity: Record<string, unknown>): Array<{
|
|
60
|
+
text: string | null | undefined;
|
|
61
|
+
field: string;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
export interface SearchParams {
|
|
65
|
+
/** User's search query */
|
|
66
|
+
query: string;
|
|
67
|
+
/** Restrict to this location slug (e.g. city) */
|
|
68
|
+
locationSlug?: string;
|
|
69
|
+
/** Restrict to this category slug (e.g. style) */
|
|
70
|
+
categorySlug?: string;
|
|
71
|
+
/** User's latitude for geo proximity */
|
|
72
|
+
lat?: number;
|
|
73
|
+
/** User's longitude for geo proximity */
|
|
74
|
+
lng?: number;
|
|
75
|
+
/** Max results to return */
|
|
76
|
+
limit?: number;
|
|
77
|
+
/** Offset for pagination */
|
|
78
|
+
offset?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface SearchResult {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
slug: string;
|
|
84
|
+
lat: number | null;
|
|
85
|
+
lng: number | null;
|
|
86
|
+
distanceKm: number | null;
|
|
87
|
+
walkingMin: number | null;
|
|
88
|
+
score: number;
|
|
89
|
+
/** @internal FTS match flag for quality gate */
|
|
90
|
+
_hasFts?: boolean;
|
|
91
|
+
/** @internal Normalized name for quality check */
|
|
92
|
+
_nameN?: string;
|
|
93
|
+
/** @internal Normalized location for quality check */
|
|
94
|
+
_locationN?: string;
|
|
95
|
+
/** @internal Normalized categories for quality check */
|
|
96
|
+
_categoriesN?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface SearchResponse<TResult extends SearchResult = SearchResult> {
|
|
99
|
+
/** Primary results (within relevance boundary) */
|
|
100
|
+
results: TResult[];
|
|
101
|
+
/** "Also within reach" results (just outside primary radius) */
|
|
102
|
+
nearby: TResult[];
|
|
103
|
+
/** True if we had location intent but found nothing nearby */
|
|
104
|
+
noLocalResults: boolean;
|
|
105
|
+
/** The place the user searched for */
|
|
106
|
+
searchedPlace: string | null;
|
|
107
|
+
/** Nearest location that has entities (when noLocalResults=true) */
|
|
108
|
+
nearestLocationWithEntities: {
|
|
109
|
+
name: string;
|
|
110
|
+
slug: string;
|
|
111
|
+
distanceKm: number;
|
|
112
|
+
count: number;
|
|
113
|
+
} | null;
|
|
114
|
+
/** Total matched before pagination */
|
|
115
|
+
totalFound: number;
|
|
116
|
+
}
|
|
117
|
+
export interface AutocompleteResult {
|
|
118
|
+
text: string;
|
|
119
|
+
type: string;
|
|
120
|
+
slug?: string;
|
|
121
|
+
}
|
|
122
|
+
export interface SearchLocale {
|
|
123
|
+
/** Strip diacritics specific to this locale */
|
|
124
|
+
stripDiacritics(text: string): string;
|
|
125
|
+
/** Stop words to remove from queries */
|
|
126
|
+
stopTokens: Set<string>;
|
|
127
|
+
/** Multi-word stop phrases (checked before single tokens) */
|
|
128
|
+
stopPhrases: string[];
|
|
129
|
+
/** Geo intent patterns (e.g. "near me", "blisko") */
|
|
130
|
+
geoPatterns: RegExp[];
|
|
131
|
+
/** Stem location names to nominative form. Returns [original, ...stems] */
|
|
132
|
+
locationStems?(token: string): string[];
|
|
133
|
+
}
|
|
134
|
+
export interface ResolverContext {
|
|
135
|
+
page: string;
|
|
136
|
+
[key: string]: unknown;
|
|
137
|
+
}
|
|
138
|
+
export interface ResolverLookups {
|
|
139
|
+
/** Normalized location name → slug */
|
|
140
|
+
locationMap: Map<string, string>;
|
|
141
|
+
/** Normalized category name/alias → slug */
|
|
142
|
+
categoryMap: Map<string, string>;
|
|
143
|
+
/** locationSlug → list of normalized area/district names */
|
|
144
|
+
areaMap: Map<string, string[]>;
|
|
145
|
+
/** locationSlug → entity count */
|
|
146
|
+
locationEntityCount?: Map<string, number>;
|
|
147
|
+
/** locationSlug → { lat, lng, name } */
|
|
148
|
+
locationGeo?: Map<string, {
|
|
149
|
+
lat: number;
|
|
150
|
+
lng: number;
|
|
151
|
+
name: string;
|
|
152
|
+
}>;
|
|
153
|
+
}
|
|
154
|
+
export type ResolverAction = {
|
|
155
|
+
action: 'route_to_location';
|
|
156
|
+
locationSlug: string;
|
|
157
|
+
categoryFilter?: string;
|
|
158
|
+
} | {
|
|
159
|
+
action: 'route_to_category';
|
|
160
|
+
categorySlug: string;
|
|
161
|
+
locationFilter?: string;
|
|
162
|
+
} | {
|
|
163
|
+
action: 'route_to_entity';
|
|
164
|
+
entitySlug: string;
|
|
165
|
+
} | {
|
|
166
|
+
action: 'filter';
|
|
167
|
+
query: string;
|
|
168
|
+
} | {
|
|
169
|
+
action: 'filter_area';
|
|
170
|
+
area: string;
|
|
171
|
+
} | {
|
|
172
|
+
action: 'filter_postcode';
|
|
173
|
+
postcode: string;
|
|
174
|
+
filter?: string;
|
|
175
|
+
} | {
|
|
176
|
+
action: 'sort_by_distance';
|
|
177
|
+
filter?: string;
|
|
178
|
+
} | {
|
|
179
|
+
action: 'show_all';
|
|
180
|
+
} | {
|
|
181
|
+
action: 'location_switch_prompt';
|
|
182
|
+
targetName: string;
|
|
183
|
+
targetSlug: string;
|
|
184
|
+
categoryFilter?: string;
|
|
185
|
+
address?: string;
|
|
186
|
+
} | {
|
|
187
|
+
action: 'already_here';
|
|
188
|
+
} | {
|
|
189
|
+
action: 'needs_server';
|
|
190
|
+
query: string;
|
|
191
|
+
} | {
|
|
192
|
+
action: 'geocode_address';
|
|
193
|
+
address: string;
|
|
194
|
+
locationSlug: string;
|
|
195
|
+
};
|
|
196
|
+
export interface TrackSearchEvent {
|
|
197
|
+
query: string;
|
|
198
|
+
queryNormalized?: string;
|
|
199
|
+
page: string;
|
|
200
|
+
locationContext?: string;
|
|
201
|
+
action: string;
|
|
202
|
+
layer?: 'client' | 'server' | 'google' | 'none';
|
|
203
|
+
resultCount?: number;
|
|
204
|
+
clickedType?: string;
|
|
205
|
+
clickedId?: string;
|
|
206
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// @nomideusz/svelte-search — Core types
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Generic search types. Apps provide a SchemaAdapter to map
|
|
5
|
+
// their DB schema to these interfaces.
|
|
6
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { SqlDialect, DatabaseClient, SchemaAdapter, SearchParams, SearchResult, SearchResponse, AutocompleteResult, SearchLocale, ResolverContext, ResolverLookups, ResolverAction, TrackSearchEvent, } from './core/types.js';
|
|
2
|
+
export { haversineKm, walkingMinutes, boundingBox, formatDistance, formatWalkingTime, walkingRoute, } from './core/geo.js';
|
|
3
|
+
export { normalize, stripDiacriticsGeneric, trigrams, trigramSimilarity, levenshtein, levenshteinSimilarity, isPostcode, hasGeoIntent, stripGeoIntent, stripStopWords, MIN_SEARCH_TOKEN_LENGTH, } from './core/normalize.js';
|
|
4
|
+
export { createSearchEngine, type SearchEngineConfig } from './core/engine.js';
|
|
5
|
+
export { createIndexer, createLookupsLoader, type IndexerConfig, type LoadLookupsConfig } from './core/indexer.js';
|
|
6
|
+
export { parseQuery, findMatchingArea, findNearestLocationWithEntities, type ParsedQuery } from './core/resolver.js';
|
|
7
|
+
export { createTracker, type TrackerConfig } from './core/track.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @nomideusz/svelte-search — Public API
|
|
2
|
+
// ============================================================
|
|
3
|
+
// Geo utilities
|
|
4
|
+
export { haversineKm, walkingMinutes, boundingBox, formatDistance, formatWalkingTime, walkingRoute, } from './core/geo.js';
|
|
5
|
+
// Normalization & similarity
|
|
6
|
+
export { normalize, stripDiacriticsGeneric, trigrams, trigramSimilarity, levenshtein, levenshteinSimilarity, isPostcode, hasGeoIntent, stripGeoIntent, stripStopWords, MIN_SEARCH_TOKEN_LENGTH, } from './core/normalize.js';
|
|
7
|
+
// Search engine
|
|
8
|
+
export { createSearchEngine } from './core/engine.js';
|
|
9
|
+
// Indexer
|
|
10
|
+
export { createIndexer, createLookupsLoader } from './core/indexer.js';
|
|
11
|
+
// Resolver
|
|
12
|
+
export { parseQuery, findMatchingArea, findNearestLocationWithEntities } from './core/resolver.js';
|
|
13
|
+
// Tracker
|
|
14
|
+
export { createTracker } from './core/track.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SearchLocale } from '../core/types.js';
|
|
2
|
+
declare function stripPolishDiacritics(text: string): string;
|
|
3
|
+
declare function polishLocationStems(token: string): string[];
|
|
4
|
+
declare const LOCATIVE_IRREGULARS: Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Polish locative case for city names.
|
|
7
|
+
* "Kraków" → "Krakowie", used in "w Krakowie" (in Kraków).
|
|
8
|
+
*/
|
|
9
|
+
export declare function polishLocative(city: string): string;
|
|
10
|
+
export declare const plLocale: SearchLocale;
|
|
11
|
+
export { polishLocationStems, stripPolishDiacritics, LOCATIVE_IRREGULARS };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// @nomideusz/svelte-search — Polish locale
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Polish-specific search features: diacritics, stop words,
|
|
5
|
+
// geo intent patterns, city name stemming, locative case.
|
|
6
|
+
// ── Diacritics ─────────────────────────────────────────────
|
|
7
|
+
const PL_DIACRITICS = {
|
|
8
|
+
'ą': 'a', 'Ą': 'A', 'ć': 'c', 'Ć': 'C', 'ę': 'e', 'Ę': 'E',
|
|
9
|
+
'ł': 'l', 'Ł': 'L', 'ń': 'n', 'Ń': 'N', 'ó': 'o', 'Ó': 'O',
|
|
10
|
+
'ś': 's', 'Ś': 'S', 'ź': 'z', 'Ź': 'Z', 'ż': 'z', 'Ż': 'Z',
|
|
11
|
+
};
|
|
12
|
+
function stripPolishDiacritics(text) {
|
|
13
|
+
let r = '';
|
|
14
|
+
for (const c of text)
|
|
15
|
+
r += PL_DIACRITICS[c] ?? c;
|
|
16
|
+
return r.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
17
|
+
}
|
|
18
|
+
// ── Stop words ─────────────────────────────────────────────
|
|
19
|
+
const PL_STOP_PHRASES = [
|
|
20
|
+
'szkola jogi', 'szkoly jogi',
|
|
21
|
+
'studio jogi', 'studia jogi',
|
|
22
|
+
'zajecia jogi', 'lekcje jogi', 'klasy jogi',
|
|
23
|
+
'yoga studio', 'yoga school', 'yoga class',
|
|
24
|
+
];
|
|
25
|
+
const PL_STOP_TOKENS = new Set([
|
|
26
|
+
// Site-generic
|
|
27
|
+
'joga', 'yoga',
|
|
28
|
+
// Polish prepositions & particles
|
|
29
|
+
'w', 'z', 'na', 'do', 'od', 'dla', 'i', 'o', 'u', 'ze',
|
|
30
|
+
'przy', 'po', 'pod', 'nad', 'za', 'przed', 'miedzy', 'lub',
|
|
31
|
+
// Common filler words
|
|
32
|
+
'sie', 'jak', 'co', 'to', 'jest', 'sa', 'nie', 'tak', 'czy',
|
|
33
|
+
'tu', 'tam', 'ten', 'ta', 'te',
|
|
34
|
+
// Common in location queries
|
|
35
|
+
'okolicy', 'okolice', 'okolica', 'okolicach', 'okolicami',
|
|
36
|
+
'poblizu', 'rejon', 'rejonie', 'rejonu',
|
|
37
|
+
'sasiedztwie', 'centrum', 'okolo',
|
|
38
|
+
'praca', 'kurs', 'kursy', 'lekcje', 'zajecia', 'treningi',
|
|
39
|
+
]);
|
|
40
|
+
// ── Geo intent patterns ────────────────────────────────────
|
|
41
|
+
const PL_GEO_PATTERNS = [
|
|
42
|
+
/\bblisko\s+mnie\b/i,
|
|
43
|
+
/\bblisko\b/i,
|
|
44
|
+
/\bniedaleko\b/i,
|
|
45
|
+
/\bw\s+poblizu\b/i,
|
|
46
|
+
/\bobok\b/i,
|
|
47
|
+
/\bw\s+okolic\w*\b/i,
|
|
48
|
+
/\bnajblizej\b/i,
|
|
49
|
+
/\bnajblizsz\w*/i,
|
|
50
|
+
/\bkolo\s+mnie\b/i,
|
|
51
|
+
/\bw\s+sasiedztwie\b/i,
|
|
52
|
+
/\bw\s+moim\s+rejonie\b/i,
|
|
53
|
+
// English (universal)
|
|
54
|
+
/\bnear\s*me\b/i,
|
|
55
|
+
/\bnearby\b/i,
|
|
56
|
+
/\bclosest\b/i,
|
|
57
|
+
/\baround\s+me\b/i,
|
|
58
|
+
];
|
|
59
|
+
// ── City name stemming ─────────────────────────────────────
|
|
60
|
+
const STEM_RULES = [
|
|
61
|
+
[/owie$/, 'ow'],
|
|
62
|
+
[/odzi$/, 'odz'],
|
|
63
|
+
[/owej$/, 'owa'],
|
|
64
|
+
[/ach$/, ''],
|
|
65
|
+
[/ami$/, ''],
|
|
66
|
+
[/nie$/, 'n'],
|
|
67
|
+
[/cie$/, 'c'],
|
|
68
|
+
[/zie$/, 'z'],
|
|
69
|
+
[/skie$/, 'sk'],
|
|
70
|
+
[/y$/, ''],
|
|
71
|
+
[/i$/, ''],
|
|
72
|
+
[/e$/, ''],
|
|
73
|
+
[/u$/, ''],
|
|
74
|
+
];
|
|
75
|
+
function stemWord(word) {
|
|
76
|
+
const stems = [word];
|
|
77
|
+
for (const [re, replacement] of STEM_RULES) {
|
|
78
|
+
if (re.test(word)) {
|
|
79
|
+
const stem = word.replace(re, replacement);
|
|
80
|
+
if (stem.length >= 3 && stem !== word)
|
|
81
|
+
stems.push(stem);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return stems;
|
|
85
|
+
}
|
|
86
|
+
function polishLocationStems(token) {
|
|
87
|
+
const words = token.split(/\s+/);
|
|
88
|
+
if (words.length === 1)
|
|
89
|
+
return stemWord(token);
|
|
90
|
+
const wordStems = words.map(w => stemWord(w));
|
|
91
|
+
const results = new Set([token]);
|
|
92
|
+
function combine(idx, current) {
|
|
93
|
+
if (idx === wordStems.length) {
|
|
94
|
+
results.add(current.join(' '));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const stem of wordStems[idx]) {
|
|
98
|
+
current.push(stem);
|
|
99
|
+
combine(idx + 1, current);
|
|
100
|
+
current.pop();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
combine(0, []);
|
|
104
|
+
return [...results];
|
|
105
|
+
}
|
|
106
|
+
// ── Polish locative case ───────────────────────────────────
|
|
107
|
+
// Used for display: "w Krakowie", "w Łodzi", "w Warszawie"
|
|
108
|
+
const LOCATIVE_IRREGULARS = {
|
|
109
|
+
'Kraków': 'Krakowie',
|
|
110
|
+
'Wrocław': 'Wrocławiu',
|
|
111
|
+
'Łódź': 'Łodzi',
|
|
112
|
+
'Poznań': 'Poznaniu',
|
|
113
|
+
'Gdańsk': 'Gdańsku',
|
|
114
|
+
'Gdynia': 'Gdyni',
|
|
115
|
+
'Szczecin': 'Szczecinie',
|
|
116
|
+
'Bydgoszcz': 'Bydgoszczy',
|
|
117
|
+
'Lublin': 'Lublinie',
|
|
118
|
+
'Białystok': 'Białymstoku',
|
|
119
|
+
'Katowice': 'Katowicach',
|
|
120
|
+
'Częstochowa': 'Częstochowie',
|
|
121
|
+
'Radom': 'Radomiu',
|
|
122
|
+
'Sosnowiec': 'Sosnowcu',
|
|
123
|
+
'Toruń': 'Toruniu',
|
|
124
|
+
'Kielce': 'Kielcach',
|
|
125
|
+
'Rzeszów': 'Rzeszowie',
|
|
126
|
+
'Gliwice': 'Gliwicach',
|
|
127
|
+
'Olsztyn': 'Olsztynie',
|
|
128
|
+
'Zabrze': 'Zabrzu',
|
|
129
|
+
'Opole': 'Opolu',
|
|
130
|
+
'Tychy': 'Tychach',
|
|
131
|
+
'Płock': 'Płocku',
|
|
132
|
+
'Elbląg': 'Elblągu',
|
|
133
|
+
'Tarnów': 'Tarnowie',
|
|
134
|
+
'Chorzów': 'Chorzowie',
|
|
135
|
+
'Bytom': 'Bytomiu',
|
|
136
|
+
'Piła': 'Pile',
|
|
137
|
+
'Legnica': 'Legnicy',
|
|
138
|
+
'Jaworzno': 'Jaworznie',
|
|
139
|
+
'Siedlce': 'Siedlcach',
|
|
140
|
+
'Mysłowice': 'Mysłowicach',
|
|
141
|
+
'Konin': 'Koninie',
|
|
142
|
+
'Inowrocław': 'Inowrocławiu',
|
|
143
|
+
'Skierniewice': 'Skierniewicach',
|
|
144
|
+
'Sopot': 'Sopocie',
|
|
145
|
+
'Warszawa': 'Warszawie',
|
|
146
|
+
'Bełchatów': 'Bełchatowie',
|
|
147
|
+
'Bielany Wrocławskie': 'Bielanach Wrocławskich',
|
|
148
|
+
'Borkowo': 'Borkowie',
|
|
149
|
+
'Brzozówka': 'Brzozówce',
|
|
150
|
+
'Czeladź': 'Czeladzi',
|
|
151
|
+
'Górki': 'Górkach',
|
|
152
|
+
'Kamionki': 'Kamionkach',
|
|
153
|
+
'Komorniki': 'Komornikach',
|
|
154
|
+
'Konstantynów Łódzki': 'Konstantynowie Łódzkim',
|
|
155
|
+
'Kowale': 'Kowalach',
|
|
156
|
+
'Koziegłowy': 'Koziegłowach',
|
|
157
|
+
'Luboń': 'Luboniu',
|
|
158
|
+
'Mierzyn': 'Mierzynie',
|
|
159
|
+
'Mikołów': 'Mikołowie',
|
|
160
|
+
'Monte': 'Monte',
|
|
161
|
+
'Niemcz': 'Niemczu',
|
|
162
|
+
'Osielsko': 'Osielsku',
|
|
163
|
+
'Owińska': 'Owińskach',
|
|
164
|
+
'Piekary Śląskie': 'Piekarach Śląskich',
|
|
165
|
+
'Pilchowice': 'Pilchowicach',
|
|
166
|
+
'Plewiska': 'Plewiskach',
|
|
167
|
+
'Przeźmierowo': 'Przeźmierowie',
|
|
168
|
+
'Sierosław': 'Sierosławiu',
|
|
169
|
+
'Skórzewo': 'Skórzewie',
|
|
170
|
+
'Suchy Dwór': 'Suchym Dworze',
|
|
171
|
+
'Suchy Las': 'Suchym Lesie',
|
|
172
|
+
'Wiry': 'Wirach',
|
|
173
|
+
'Wola Kopcowa': 'Woli Kopcowej',
|
|
174
|
+
'Wysoka': 'Wysokiej',
|
|
175
|
+
'Zgierz': 'Zgierzu',
|
|
176
|
+
// Adjective-level irregulars (for multi-word names)
|
|
177
|
+
'Ruda': 'Rudzie', 'Wola': 'Woli', 'Dąbrowa': 'Dąbrowie',
|
|
178
|
+
'Nowy': 'Nowym', 'Nowa': 'Nowej', 'Stary': 'Starym', 'Stara': 'Starej',
|
|
179
|
+
'Wielka': 'Wielkiej', 'Wielki': 'Wielkim',
|
|
180
|
+
'Mały': 'Małym', 'Mała': 'Małej',
|
|
181
|
+
'Dolna': 'Dolnej', 'Dolny': 'Dolnym',
|
|
182
|
+
'Górna': 'Górnej', 'Górny': 'Górnym',
|
|
183
|
+
'Suchy': 'Suchym', 'Sucha': 'Suchej',
|
|
184
|
+
};
|
|
185
|
+
function locativeWord(w) {
|
|
186
|
+
if (LOCATIVE_IRREGULARS[w])
|
|
187
|
+
return LOCATIVE_IRREGULARS[w];
|
|
188
|
+
// Adjective endings
|
|
189
|
+
if (w.endsWith('owa'))
|
|
190
|
+
return w.slice(0, -1) + 'ej';
|
|
191
|
+
if (w.endsWith('owy'))
|
|
192
|
+
return w.slice(0, -1) + 'ym';
|
|
193
|
+
if (w.endsWith('ska'))
|
|
194
|
+
return w.slice(0, -1) + 'iej';
|
|
195
|
+
if (w.endsWith('cka'))
|
|
196
|
+
return w.slice(0, -1) + 'iej';
|
|
197
|
+
if (w.endsWith('ski') || w.endsWith('cki'))
|
|
198
|
+
return w + 'm';
|
|
199
|
+
if (w.endsWith('na'))
|
|
200
|
+
return w.slice(0, -1) + 'ej';
|
|
201
|
+
if (w.endsWith('ny'))
|
|
202
|
+
return w.slice(0, -1) + 'ym';
|
|
203
|
+
if (w.endsWith('cza'))
|
|
204
|
+
return w.slice(0, -1) + 'ej';
|
|
205
|
+
if (w.endsWith('sza'))
|
|
206
|
+
return w.slice(0, -1) + 'ej';
|
|
207
|
+
if (w.endsWith('ła'))
|
|
208
|
+
return w.slice(0, -1) + 'ej';
|
|
209
|
+
if (w.endsWith('ły'))
|
|
210
|
+
return w.slice(0, -1) + 'ym';
|
|
211
|
+
// Noun endings
|
|
212
|
+
if (w.endsWith('ów'))
|
|
213
|
+
return w.slice(0, -2) + 'owie';
|
|
214
|
+
if (w.endsWith('ław'))
|
|
215
|
+
return w + 'iu';
|
|
216
|
+
if (w.endsWith('in') || w.endsWith('yn'))
|
|
217
|
+
return w + 'ie';
|
|
218
|
+
if (w.endsWith('ań') || w.endsWith('eń') || w.endsWith('uń'))
|
|
219
|
+
return w.slice(0, -1) + 'niu';
|
|
220
|
+
if (w.endsWith('ek'))
|
|
221
|
+
return w.slice(0, -2) + 'ku';
|
|
222
|
+
if (w.endsWith('ice') || w.endsWith('yce'))
|
|
223
|
+
return w.slice(0, -1) + 'ach';
|
|
224
|
+
if (w.endsWith('la'))
|
|
225
|
+
return w.slice(0, -1) + 'i';
|
|
226
|
+
if (w.endsWith('a'))
|
|
227
|
+
return w.slice(0, -1) + 'ie';
|
|
228
|
+
if (w.endsWith('o'))
|
|
229
|
+
return w.slice(0, -1) + 'ie';
|
|
230
|
+
if (w.endsWith('e'))
|
|
231
|
+
return w.slice(0, -1) + 'u';
|
|
232
|
+
if (w.endsWith('y'))
|
|
233
|
+
return w.slice(0, -1) + 'ach';
|
|
234
|
+
if (/(?:k|g|ch|sz|cz|rz|ż|ąg|ąk)$/.test(w))
|
|
235
|
+
return w + 'u';
|
|
236
|
+
return w + 'ie';
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Polish locative case for city names.
|
|
240
|
+
* "Kraków" → "Krakowie", used in "w Krakowie" (in Kraków).
|
|
241
|
+
*/
|
|
242
|
+
export function polishLocative(city) {
|
|
243
|
+
const c = city.trim();
|
|
244
|
+
if (LOCATIVE_IRREGULARS[c])
|
|
245
|
+
return LOCATIVE_IRREGULARS[c];
|
|
246
|
+
// Multi-word names: decline each word separately
|
|
247
|
+
const parts = c.split(/(\s+|-)/);
|
|
248
|
+
if (parts.filter(p => p.trim() && p !== '-').length > 1) {
|
|
249
|
+
return parts.map(p => {
|
|
250
|
+
if (!p.trim() || p === '-')
|
|
251
|
+
return p;
|
|
252
|
+
return locativeWord(p);
|
|
253
|
+
}).join('');
|
|
254
|
+
}
|
|
255
|
+
return locativeWord(c);
|
|
256
|
+
}
|
|
257
|
+
// ── Export locale ──────────────────────────────────────────
|
|
258
|
+
export const plLocale = {
|
|
259
|
+
stripDiacritics: stripPolishDiacritics,
|
|
260
|
+
stopTokens: PL_STOP_TOKENS,
|
|
261
|
+
stopPhrases: PL_STOP_PHRASES,
|
|
262
|
+
geoPatterns: PL_GEO_PATTERNS,
|
|
263
|
+
locationStems: polishLocationStems,
|
|
264
|
+
};
|
|
265
|
+
// Re-export individual pieces for apps that need them directly
|
|
266
|
+
export { polishLocationStems, stripPolishDiacritics, LOCATIVE_IRREGULARS };
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nomideusz/svelte-search",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Full-text search engine for Svelte 5 apps — FTS5, trigram fuzzy matching, geo proximity, autocomplete, Polish locale support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"svelte": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"svelte": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./locales/pl": {
|
|
16
|
+
"types": "./dist/locales/pl.d.ts",
|
|
17
|
+
"default": "./dist/locales/pl.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"!dist/**/*.test.*",
|
|
24
|
+
"!dist/**/*.spec.*"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"svelte": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
31
|
+
"@sveltejs/kit": "^2.50.2",
|
|
32
|
+
"@sveltejs/package": "^2.5.7",
|
|
33
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
34
|
+
"svelte": "^5.51.0",
|
|
35
|
+
"svelte-check": "^4.3.6",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vite": "^7.3.1",
|
|
38
|
+
"vitest": "^4.0.18"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"svelte",
|
|
42
|
+
"search",
|
|
43
|
+
"fts5",
|
|
44
|
+
"trigram",
|
|
45
|
+
"fuzzy",
|
|
46
|
+
"geo",
|
|
47
|
+
"autocomplete",
|
|
48
|
+
"polish",
|
|
49
|
+
"sqlite",
|
|
50
|
+
"postgres"
|
|
51
|
+
],
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/nomideusz/asini"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"dev": "vite dev",
|
|
58
|
+
"build": "vite build",
|
|
59
|
+
"package": "svelte-kit sync && svelte-package",
|
|
60
|
+
"preview": "vite preview",
|
|
61
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
62
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"test:watch": "vitest"
|
|
65
|
+
}
|
|
66
|
+
}
|