@iola_adm/iola-cli 0.2.5 → 0.2.6
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 +7 -0
- package/package.json +1 -1
- package/skills/geo/SKILL.md +20 -0
- package/src/cli.js +507 -11
- package/wiki/Yandex-Geocoder-API-key.md +1 -1
- package/wiki//320/232/320/276/320/274/320/260/320/275/320/264/321/213.md +6 -0
- package/wiki//320/241/320/272/320/270/320/273/320/273/321/213-/320/264/320/273/321/217-/320/266/320/270/321/202/320/265/320/273/320/265/320/271.md +19 -15
package/README.md
CHANGED
|
@@ -162,9 +162,16 @@ iola ai setup gigachat --model GigaChat-2
|
|
|
162
162
|
iola geo key set yandex
|
|
163
163
|
iola geo key doctor
|
|
164
164
|
iola geo geocode "Йошкар-Ола, улица Петрова, 15"
|
|
165
|
+
iola geo nearby "Йошкар-Ола, улица Петрова, 15" --dataset all --limit 5
|
|
166
|
+
iola geo distance --from "Петрова 15" --to "школа 7"
|
|
167
|
+
iola geo map-link "школа 7"
|
|
168
|
+
iola geo resolve "садик золотой петушок"
|
|
169
|
+
iola geo route-context "школа 7"
|
|
170
|
+
iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
165
171
|
```
|
|
166
172
|
|
|
167
173
|
Инструкция по получению ключа: [Yandex Geocoder API key](https://github.com/adm-iola/iola-cli/wiki/Yandex-Geocoder-API-key).
|
|
174
|
+
Список сценариев: [Скиллы для жителей](https://github.com/adm-iola/iola-cli/wiki/Скиллы-для-жителей).
|
|
168
175
|
|
|
169
176
|
Зарубежные API-ключи:
|
|
170
177
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: geo
|
|
3
|
+
description: Geo-сценарии для жителей: что рядом, карта, расстояние, уточнение места и населенного пункта.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Используй geo-функции CLI и Yandex Geocoder API для вопросов о местоположении.
|
|
7
|
+
|
|
8
|
+
Основные пользовательские сценарии:
|
|
9
|
+
|
|
10
|
+
- `nearby` - найти школы, детские сады и другие будущие городские объекты рядом с адресом.
|
|
11
|
+
- `route-context` - объяснить, где находится объект: адрес, ориентиры, что рядом, ссылка на карту.
|
|
12
|
+
- `address-to-services` - по адресу жителя подобрать ближайшие подключенные городские объекты.
|
|
13
|
+
- `map-link` - дать ссылку на карту и координаты.
|
|
14
|
+
- `place-resolver` - уточнить криво написанное место, адрес или название объекта.
|
|
15
|
+
- `distance` - посчитать расстояние по прямой между адресом и объектом или между двумя объектами.
|
|
16
|
+
- `area-filter` - не путать населенные пункты и районы, например Йошкар-Олу и Семеновку.
|
|
17
|
+
|
|
18
|
+
Не придумывай координаты, расстояния и адреса. Если геокодер или слой данных не дал результата, прямо скажи, что данных недостаточно.
|
|
19
|
+
|
|
20
|
+
Маршрут по дорогам не рассчитывается: расстояние в CLI считается по координатам как расстояние по прямой, если явно не подключен отдельный маршрутизатор.
|
package/src/cli.js
CHANGED
|
@@ -228,7 +228,7 @@ const DEFAULT_AI_CONFIG = {
|
|
|
228
228
|
suggestions: true,
|
|
229
229
|
},
|
|
230
230
|
skills: {
|
|
231
|
-
enabled: ["education", "open-data", "reports", "local-model", "local-files", "browser-agent"],
|
|
231
|
+
enabled: ["education", "open-data", "geo", "reports", "local-model", "local-files", "browser-agent"],
|
|
232
232
|
},
|
|
233
233
|
daemon: {
|
|
234
234
|
host: "127.0.0.1",
|
|
@@ -5343,6 +5343,36 @@ async function handleGeo(args) {
|
|
|
5343
5343
|
return;
|
|
5344
5344
|
}
|
|
5345
5345
|
|
|
5346
|
+
if (command === "nearby") {
|
|
5347
|
+
await geoNearby([subcommand, ...rest].filter(Boolean));
|
|
5348
|
+
return;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
if (command === "distance") {
|
|
5352
|
+
await geoDistance([subcommand, ...rest].filter(Boolean));
|
|
5353
|
+
return;
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
if (command === "map-link") {
|
|
5357
|
+
await geoMapLink([subcommand, ...rest].filter(Boolean));
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
|
|
5361
|
+
if (command === "resolve") {
|
|
5362
|
+
await geoResolve([subcommand, ...rest].filter(Boolean));
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
if (command === "route-context") {
|
|
5367
|
+
await geoRouteContext([subcommand, ...rest].filter(Boolean));
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
if (command === "services") {
|
|
5372
|
+
await geoServices([subcommand, ...rest].filter(Boolean));
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5346
5376
|
if (command === "doctor" || command === "status") {
|
|
5347
5377
|
await printGeoKeyStatus({ check: command === "doctor" });
|
|
5348
5378
|
return;
|
|
@@ -5353,7 +5383,13 @@ async function handleGeo(args) {
|
|
|
5353
5383
|
iola geo key status
|
|
5354
5384
|
iola geo key doctor
|
|
5355
5385
|
iola geo key delete yandex
|
|
5356
|
-
iola geo geocode "Йошкар-Ола, ул. Петрова, 15"
|
|
5386
|
+
iola geo geocode "Йошкар-Ола, ул. Петрова, 15"
|
|
5387
|
+
iola geo nearby "Йошкар-Ола, ул. Петрова, 15" --dataset all --limit 5
|
|
5388
|
+
iola geo distance --from "Йошкар-Ола, ул. Петрова, 15" --to "школа 7"
|
|
5389
|
+
iola geo map-link "школа 7"
|
|
5390
|
+
iola geo resolve "садик золотой петушок"
|
|
5391
|
+
iola geo route-context "школа 7"
|
|
5392
|
+
iola geo services "Йошкар-Ола, ул. Петрова, 15"`);
|
|
5357
5393
|
}
|
|
5358
5394
|
|
|
5359
5395
|
async function handleGeoKey(args) {
|
|
@@ -5433,6 +5469,56 @@ async function geoGeocode(args) {
|
|
|
5433
5469
|
printKeyValue(result);
|
|
5434
5470
|
}
|
|
5435
5471
|
|
|
5472
|
+
async function geoNearby(args) {
|
|
5473
|
+
const options = parseOptions(args);
|
|
5474
|
+
const query = options._.join(" ").trim();
|
|
5475
|
+
if (!query) throw new Error('Адрес обязателен. Пример: iola geo nearby "Йошкар-Ола, ул. Петрова, 15" --dataset schools');
|
|
5476
|
+
const answer = await buildNearbyAnswer(query, {
|
|
5477
|
+
dataset: normalizeGeoDataset(options.dataset || inferGeoDataset(query)),
|
|
5478
|
+
limit: Number(options.limit || 5),
|
|
5479
|
+
radius: Number(options.radius || 0),
|
|
5480
|
+
});
|
|
5481
|
+
console.log(answer);
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
async function geoDistance(args) {
|
|
5485
|
+
const options = parseOptions(args);
|
|
5486
|
+
const from = options.from || options.address || options._.join(" ").split(/\s+(?:до|и|->)\s+/iu)[0]?.trim();
|
|
5487
|
+
const to = options.to || options._.join(" ").split(/\s+(?:до|и|->)\s+/iu)[1]?.trim();
|
|
5488
|
+
if (!from || !to) throw new Error('Нужны две точки. Пример: iola geo distance --from "Петрова 15" --to "школа 7"');
|
|
5489
|
+
const answer = await buildDistanceAnswer(from, to);
|
|
5490
|
+
console.log(answer);
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
async function geoMapLink(args) {
|
|
5494
|
+
const query = args.join(" ").trim();
|
|
5495
|
+
if (!query) throw new Error('Объект или адрес обязателен. Пример: iola geo map-link "школа 7"');
|
|
5496
|
+
const answer = await buildMapLinkAnswer(query);
|
|
5497
|
+
console.log(answer);
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5500
|
+
async function geoResolve(args) {
|
|
5501
|
+
const query = args.join(" ").trim();
|
|
5502
|
+
if (!query) throw new Error('Место или объект обязателен. Пример: iola geo resolve "садик золотой петушок"');
|
|
5503
|
+
const answer = await buildPlaceResolverAnswer(query);
|
|
5504
|
+
console.log(answer);
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
async function geoRouteContext(args) {
|
|
5508
|
+
const query = args.join(" ").trim();
|
|
5509
|
+
if (!query) throw new Error('Объект или адрес обязателен. Пример: iola geo route-context "школа 7"');
|
|
5510
|
+
const answer = await buildRouteContextAnswer(query);
|
|
5511
|
+
console.log(answer);
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
async function geoServices(args) {
|
|
5515
|
+
const options = parseOptions(args);
|
|
5516
|
+
const query = options._.join(" ").trim();
|
|
5517
|
+
if (!query) throw new Error('Адрес обязателен. Пример: iola geo services "Йошкар-Ола, ул. Петрова, 15"');
|
|
5518
|
+
const answer = await buildAddressToServicesAnswer(query, { limit: Number(options.limit || 3) });
|
|
5519
|
+
console.log(answer);
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5436
5522
|
async function checkYandexGeocoderKey(options = {}) {
|
|
5437
5523
|
try {
|
|
5438
5524
|
const result = await callYandexGeocoder("Йошкар-Ола");
|
|
@@ -5462,12 +5548,7 @@ async function callYandexGeocoder(query) {
|
|
|
5462
5548
|
url.searchParams.set("lang", "ru_RU");
|
|
5463
5549
|
url.searchParams.set("results", "1");
|
|
5464
5550
|
|
|
5465
|
-
|
|
5466
|
-
try {
|
|
5467
|
-
response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
5468
|
-
} catch (error) {
|
|
5469
|
-
throw new Error(formatProviderFetchError("Yandex Geocoder", error));
|
|
5470
|
-
}
|
|
5551
|
+
const response = await fetchYandexGeocoderWithRetry(url);
|
|
5471
5552
|
|
|
5472
5553
|
if (!response.ok) {
|
|
5473
5554
|
const text = await response.text();
|
|
@@ -5492,6 +5573,401 @@ async function callYandexGeocoder(query) {
|
|
|
5492
5573
|
};
|
|
5493
5574
|
}
|
|
5494
5575
|
|
|
5576
|
+
async function fetchYandexGeocoderWithRetry(url) {
|
|
5577
|
+
let lastError = null;
|
|
5578
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
5579
|
+
try {
|
|
5580
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
5581
|
+
if (response.status !== 429 || attempt === 2) return response;
|
|
5582
|
+
await sleep(700 * (attempt + 1));
|
|
5583
|
+
} catch (error) {
|
|
5584
|
+
lastError = error;
|
|
5585
|
+
if (attempt === 2) break;
|
|
5586
|
+
await sleep(500 * (attempt + 1));
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
throw new Error(formatProviderFetchError("Yandex Geocoder", lastError));
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
const geoMemoryCache = new Map();
|
|
5593
|
+
|
|
5594
|
+
async function buildGeoDirectAnswer(question) {
|
|
5595
|
+
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
5596
|
+
if (!isGeoQuestion(normalized)) return "";
|
|
5597
|
+
const place = detectEducationPlace(question);
|
|
5598
|
+
if (place && !place.supported && /(школ|сош|лице|гимнази|сад|детсад|детск\w*\s+сад|садик)/iu.test(normalized)) {
|
|
5599
|
+
return formatUnsupportedEducationGeoPlace(place);
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5602
|
+
try {
|
|
5603
|
+
if (/(расстояни|как далеко|далеко ли|ближе)/iu.test(normalized)) {
|
|
5604
|
+
const pair = extractDistancePair(question);
|
|
5605
|
+
if (pair.from && pair.to) return buildDistanceAnswer(pair.from, pair.to);
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
if (/(карт[аеу]|ссылк.*карт|открыть.*карт)/iu.test(normalized)) {
|
|
5609
|
+
const query = cleanupGeoObjectQuery(question);
|
|
5610
|
+
if (query) return buildMapLinkAnswer(query);
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5613
|
+
if (/(где находится|как пройти|как добраться|что рядом|ориентир)/iu.test(normalized)) {
|
|
5614
|
+
const query = cleanupGeoObjectQuery(question);
|
|
5615
|
+
if (query) return buildRouteContextAnswer(query);
|
|
5616
|
+
}
|
|
5617
|
+
|
|
5618
|
+
if (/(мой адрес|живу|по адресу)/iu.test(normalized)) {
|
|
5619
|
+
const address = extractAddressForNearby(question);
|
|
5620
|
+
if (address) return buildAddressToServicesAnswer(address, { limit: 3 });
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
if (/(рядом|поблизости|ближайш|ближе|около|возле)/iu.test(normalized)) {
|
|
5624
|
+
const address = extractAddressForNearby(question);
|
|
5625
|
+
if (address) return buildNearbyAnswer(address, { dataset: normalizeGeoDataset(inferGeoDataset(question)), limit: 5 });
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
if (/(уточни место|какой населенный пункт|какой район|фильтр.*район|фильтр.*населен|не путай.*населен)/iu.test(normalized)) {
|
|
5629
|
+
const query = cleanupGeoObjectQuery(question);
|
|
5630
|
+
if (query) return buildPlaceResolverAnswer(query);
|
|
5631
|
+
}
|
|
5632
|
+
} catch (error) {
|
|
5633
|
+
if (/Yandex Geocoder API key не найден/iu.test(String(error?.message || ""))) return "";
|
|
5634
|
+
return `Не смог выполнить geo-запрос: ${error instanceof Error ? error.message : String(error)}`;
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
return "";
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
function formatUnsupportedEducationGeoPlace(place) {
|
|
5641
|
+
return `В текущих открытых данных iola-cli есть данные городского округа Йошкар-Ола. Данных по ${place.locative || place.label} в подключенных слоях нет, поэтому не могу надежно ответить по этому объекту.`;
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
function isGeoQuestion(normalized) {
|
|
5645
|
+
return /(рядом|поблизости|ближайш|ближе|расстояни|как далеко|карт[аеу]|где находится|как пройти|как добраться|мой адрес|живу|по адресу|ориентир|уточни место|какой населенный пункт|какой район|фильтр.*район|фильтр.*населен)/iu.test(normalized);
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
async function buildNearbyAnswer(address, options = {}) {
|
|
5649
|
+
const origin = await resolveGeoPoint(address);
|
|
5650
|
+
const dataset = normalizeGeoDataset(options.dataset || "all");
|
|
5651
|
+
const limit = Math.max(1, Number(options.limit || 5));
|
|
5652
|
+
const radius = Number(options.radius || 0);
|
|
5653
|
+
const candidates = await getGeoCandidates(dataset);
|
|
5654
|
+
const ranked = (await asyncMapLimit(candidates, 6, async (item) => {
|
|
5655
|
+
const point = item.address
|
|
5656
|
+
? await geocodeCached(normalizeAddressForGeocoder(item.address)).catch(() => null)
|
|
5657
|
+
: await resolveGeoPoint(item.name).catch(() => null);
|
|
5658
|
+
if (!point?.lat || !point?.lon) return null;
|
|
5659
|
+
const distanceMeters = haversineMeters(origin, point);
|
|
5660
|
+
return { ...item, point, distanceMeters };
|
|
5661
|
+
})).filter(Boolean)
|
|
5662
|
+
.filter((item) => !radius || item.distanceMeters <= radius)
|
|
5663
|
+
.sort((a, b) => a.distanceMeters - b.distanceMeters)
|
|
5664
|
+
.slice(0, limit);
|
|
5665
|
+
|
|
5666
|
+
if (ranked.length === 0) return `Рядом с адресом "${address}" не нашел объектов в подключенных слоях.`;
|
|
5667
|
+
|
|
5668
|
+
return [
|
|
5669
|
+
`Ближайшие объекты к адресу: ${origin.address || address}`,
|
|
5670
|
+
...ranked.map((item, index) => `${index + 1}. ${item.name} — ${formatDistance(item.distanceMeters)}${item.address ? `\n Адрес: ${item.address}` : ""}${item.inn ? `\n ИНН: ${item.inn}` : ""}${item.point.map ? `\n Карта: ${item.point.map}` : ""}`),
|
|
5671
|
+
`Источник: Yandex Geocoder + слои ${dataset === "all" ? "schools, kindergartens" : dataset}.`,
|
|
5672
|
+
].join("\n");
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
async function buildDistanceAnswer(fromQuery, toQuery) {
|
|
5676
|
+
const from = await resolveGeoPoint(fromQuery);
|
|
5677
|
+
const to = await resolveGeoPoint(toQuery);
|
|
5678
|
+
const distance = haversineMeters(from, to);
|
|
5679
|
+
return [
|
|
5680
|
+
`Расстояние по прямой: ${formatDistance(distance)}.`,
|
|
5681
|
+
`От: ${from.name || from.address || fromQuery}${from.address && from.address !== from.name ? ` (${from.address})` : ""}`,
|
|
5682
|
+
`До: ${to.name || to.address || toQuery}${to.address && to.address !== to.name ? ` (${to.address})` : ""}`,
|
|
5683
|
+
to.map ? `Карта точки назначения: ${to.map}` : "",
|
|
5684
|
+
"Это расстояние по координатам, не маршрут по дорогам.",
|
|
5685
|
+
].filter(Boolean).join("\n");
|
|
5686
|
+
}
|
|
5687
|
+
|
|
5688
|
+
async function buildMapLinkAnswer(query) {
|
|
5689
|
+
const point = await resolveGeoPoint(query);
|
|
5690
|
+
return [
|
|
5691
|
+
point.name || query,
|
|
5692
|
+
point.address ? `Адрес: ${point.address}` : "",
|
|
5693
|
+
point.coordinates ? `Координаты: ${point.coordinates}` : "",
|
|
5694
|
+
point.map ? `Карта: ${point.map}` : "",
|
|
5695
|
+
].filter(Boolean).join("\n");
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5698
|
+
async function buildPlaceResolverAnswer(query) {
|
|
5699
|
+
const match = await resolveGeoEntity(query).catch(() => null);
|
|
5700
|
+
if (match) {
|
|
5701
|
+
return [
|
|
5702
|
+
`Нашел объект: ${match.name}`,
|
|
5703
|
+
match.address ? `Адрес: ${match.address}` : "",
|
|
5704
|
+
match.inn ? `ИНН: ${match.inn}` : "",
|
|
5705
|
+
match.layer ? `Слой: ${match.layer}` : "",
|
|
5706
|
+
match.map ? `Карта: ${match.map}` : "",
|
|
5707
|
+
].filter(Boolean).join("\n");
|
|
5708
|
+
}
|
|
5709
|
+
const point = await callYandexGeocoder(query);
|
|
5710
|
+
if (!point) return `Не смог уточнить место: ${query}`;
|
|
5711
|
+
return [
|
|
5712
|
+
`Уточнил место через геокодер: ${point.name || query}`,
|
|
5713
|
+
point.address ? `Адрес: ${point.address}` : "",
|
|
5714
|
+
point.precision ? `Точность: ${point.precision}` : "",
|
|
5715
|
+
point.coordinates ? `Координаты: ${point.coordinates}` : "",
|
|
5716
|
+
point.map ? `Карта: ${point.map}` : "",
|
|
5717
|
+
].filter(Boolean).join("\n");
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
async function buildRouteContextAnswer(query) {
|
|
5721
|
+
const point = await resolveGeoPoint(query);
|
|
5722
|
+
const nearby = await buildNearbyAnswer(point.address || query, { dataset: "all", limit: 3 });
|
|
5723
|
+
return [
|
|
5724
|
+
`${point.name || query}`,
|
|
5725
|
+
point.address ? `Адрес: ${point.address}` : "",
|
|
5726
|
+
point.coordinates ? `Координаты: ${point.coordinates}` : "",
|
|
5727
|
+
point.map ? `Карта: ${point.map}` : "",
|
|
5728
|
+
"",
|
|
5729
|
+
nearby,
|
|
5730
|
+
].filter(Boolean).join("\n");
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5733
|
+
async function buildAddressToServicesAnswer(address, options = {}) {
|
|
5734
|
+
const limit = Math.max(1, Number(options.limit || 3));
|
|
5735
|
+
const schools = await buildNearbyAnswer(address, { dataset: "schools", limit });
|
|
5736
|
+
const kindergartens = await buildNearbyAnswer(address, { dataset: "kindergartens", limit });
|
|
5737
|
+
return [
|
|
5738
|
+
`По адресу "${address}" ближайшие подключенные городские объекты:`,
|
|
5739
|
+
"",
|
|
5740
|
+
"Школы:",
|
|
5741
|
+
stripNearbyHeader(schools),
|
|
5742
|
+
"",
|
|
5743
|
+
"Детские сады:",
|
|
5744
|
+
stripNearbyHeader(kindergartens),
|
|
5745
|
+
].join("\n");
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5748
|
+
function stripNearbyHeader(text) {
|
|
5749
|
+
return String(text || "").split("\n").filter((line) => !line.startsWith("Ближайшие объекты к адресу:")).join("\n");
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
async function resolveGeoPoint(query) {
|
|
5753
|
+
const entity = await resolveGeoEntity(query).catch(() => null);
|
|
5754
|
+
if (entity?.address) {
|
|
5755
|
+
const point = await geocodeCached(normalizeAddressForGeocoder(entity.address));
|
|
5756
|
+
return mergeGeoEntityPoint(entity, point);
|
|
5757
|
+
}
|
|
5758
|
+
return geocodeCached(query);
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5761
|
+
async function resolveGeoEntity(query) {
|
|
5762
|
+
const dataset = normalizeGeoDataset(inferGeoDataset(query));
|
|
5763
|
+
let candidates = await getGeoCandidates(dataset);
|
|
5764
|
+
const normalized = normalizeGeoText(query);
|
|
5765
|
+
const number = extractEntityNumberFromQuestion(query, dataset === "kindergartens" ? "kindergartens" : "schools");
|
|
5766
|
+
const place = detectEducationPlace(query);
|
|
5767
|
+
if (place && !place.supported) return null;
|
|
5768
|
+
if (place?.supported) {
|
|
5769
|
+
const placeMatches = candidates.filter((item) => geoItemMatchesPlace(item, place));
|
|
5770
|
+
if (placeMatches.length > 0) candidates = placeMatches;
|
|
5771
|
+
}
|
|
5772
|
+
if (number) {
|
|
5773
|
+
const exactNumberMatches = candidates.filter((item) => itemNameHasNumber(item, number));
|
|
5774
|
+
if (exactNumberMatches.length === 1) {
|
|
5775
|
+
const point = exactNumberMatches[0].address ? await geocodeCached(normalizeAddressForGeocoder(exactNumberMatches[0].address)).catch(() => null) : null;
|
|
5776
|
+
return mergeGeoEntityPoint(exactNumberMatches[0], point);
|
|
5777
|
+
}
|
|
5778
|
+
if (exactNumberMatches.length > 1) {
|
|
5779
|
+
const scoped = place?.supported
|
|
5780
|
+
? exactNumberMatches.filter((item) => geoItemMatchesPlace(item, place))
|
|
5781
|
+
: exactNumberMatches;
|
|
5782
|
+
const bestExact = scoped[0] || exactNumberMatches[0];
|
|
5783
|
+
const point = bestExact.address ? await geocodeCached(normalizeAddressForGeocoder(bestExact.address)).catch(() => null) : null;
|
|
5784
|
+
return mergeGeoEntityPoint(bestExact, point);
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
const scored = candidates.map((item) => ({ ...item, score: scoreGeoCandidate(item, normalized, number, place) }))
|
|
5788
|
+
.filter((item) => item.score > 0)
|
|
5789
|
+
.sort((a, b) => b.score - a.score);
|
|
5790
|
+
const best = scored[0];
|
|
5791
|
+
if (!best) return null;
|
|
5792
|
+
const point = best.address ? await geocodeCached(normalizeAddressForGeocoder(best.address)).catch(() => null) : null;
|
|
5793
|
+
return mergeGeoEntityPoint(best, point);
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5796
|
+
function mergeGeoEntityPoint(entity, point) {
|
|
5797
|
+
if (!point) return entity;
|
|
5798
|
+
return {
|
|
5799
|
+
...entity,
|
|
5800
|
+
geoName: point.name || "",
|
|
5801
|
+
geocodedAddress: point.address || "",
|
|
5802
|
+
precision: point.precision || "",
|
|
5803
|
+
coordinates: point.coordinates || "",
|
|
5804
|
+
map: point.map || "",
|
|
5805
|
+
lat: point.lat,
|
|
5806
|
+
lon: point.lon,
|
|
5807
|
+
};
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
function scoreGeoCandidate(item, normalizedQuery, number, place) {
|
|
5811
|
+
const name = normalizeGeoText(item.name);
|
|
5812
|
+
const address = normalizeGeoText(item.address);
|
|
5813
|
+
let score = 0;
|
|
5814
|
+
if (number && itemNameHasNumber(item, number)) score += 20;
|
|
5815
|
+
for (const token of normalizeGeoText(normalizedQuery).split(/\s+/).filter((part) => part.length >= 3)) {
|
|
5816
|
+
if (name.includes(token)) score += 3;
|
|
5817
|
+
if (address.includes(token)) score += 1;
|
|
5818
|
+
}
|
|
5819
|
+
if (place?.supported && geoItemMatchesPlace(item, place)) score += 8;
|
|
5820
|
+
if (/(сад|детсад|садик)/iu.test(normalizedQuery) && item.layer === "kindergartens") score += 5;
|
|
5821
|
+
if (/(школ|лице|гимнази)/iu.test(normalizedQuery) && item.layer === "schools") score += 5;
|
|
5822
|
+
return score;
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
function geoItemMatchesPlace(item, place) {
|
|
5826
|
+
if (!place?.supported) return false;
|
|
5827
|
+
const haystack = normalizeGeoText(`${item.name || ""} ${item.address || ""}`);
|
|
5828
|
+
const aliases = [place.label, ...(place.aliases || [])].map(normalizeGeoText).filter(Boolean);
|
|
5829
|
+
return aliases.some((alias) => haystack.includes(alias));
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
async function getGeoCandidates(dataset = "all") {
|
|
5833
|
+
const layers = dataset === "all" ? ["schools", "kindergartens"] : [dataset];
|
|
5834
|
+
const result = [];
|
|
5835
|
+
for (const layer of layers) {
|
|
5836
|
+
const items = normalizeItems(await fetchAllApiItems(`${await getApiBaseUrl()}/${DATASETS[layer].endpoint}`))
|
|
5837
|
+
.map(selectPublicSummary)
|
|
5838
|
+
.filter((item) => item.name || item.address)
|
|
5839
|
+
.map((item) => ({ ...item, layer }));
|
|
5840
|
+
result.push(...items);
|
|
5841
|
+
}
|
|
5842
|
+
return result;
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
async function geocodeCached(query) {
|
|
5846
|
+
const key = normalizeGeoText(query);
|
|
5847
|
+
if (geoMemoryCache.has(key)) return geoMemoryCache.get(key);
|
|
5848
|
+
const value = await callYandexGeocoder(query);
|
|
5849
|
+
if (!value) throw new Error(`Yandex Geocoder не вернул результат для: ${query}`);
|
|
5850
|
+
const parsed = parseCoordinates(value.coordinates);
|
|
5851
|
+
const point = { ...value, ...parsed };
|
|
5852
|
+
geoMemoryCache.set(key, point);
|
|
5853
|
+
return point;
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
function parseCoordinates(coordinates) {
|
|
5857
|
+
const [lat, lon] = String(coordinates || "").split(",").map((part) => Number(part.trim()));
|
|
5858
|
+
return { lat, lon };
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
async function asyncMapLimit(items, limit, mapper) {
|
|
5862
|
+
const source = Array.from(items || []);
|
|
5863
|
+
if (source.length === 0) return [];
|
|
5864
|
+
const results = new Array(source.length);
|
|
5865
|
+
let next = 0;
|
|
5866
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, source.length)) }, async () => {
|
|
5867
|
+
while (next < source.length) {
|
|
5868
|
+
const index = next;
|
|
5869
|
+
next += 1;
|
|
5870
|
+
results[index] = await mapper(source[index], index);
|
|
5871
|
+
}
|
|
5872
|
+
});
|
|
5873
|
+
await Promise.all(workers);
|
|
5874
|
+
return results;
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
function haversineMeters(a, b) {
|
|
5878
|
+
const radius = 6371000;
|
|
5879
|
+
const lat1 = toRadians(Number(a.lat));
|
|
5880
|
+
const lat2 = toRadians(Number(b.lat));
|
|
5881
|
+
const deltaLat = toRadians(Number(b.lat) - Number(a.lat));
|
|
5882
|
+
const deltaLon = toRadians(Number(b.lon) - Number(a.lon));
|
|
5883
|
+
const x = Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) ** 2;
|
|
5884
|
+
return 2 * radius * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x));
|
|
5885
|
+
}
|
|
5886
|
+
|
|
5887
|
+
function toRadians(value) {
|
|
5888
|
+
return value * Math.PI / 180;
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
function formatDistance(meters) {
|
|
5892
|
+
if (!Number.isFinite(meters)) return "-";
|
|
5893
|
+
if (meters < 1000) return `${Math.round(meters)} м`;
|
|
5894
|
+
return `${(meters / 1000).toFixed(meters < 10000 ? 1 : 0)} км`;
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5897
|
+
function normalizeGeoDataset(value) {
|
|
5898
|
+
const text = String(value || "").toLocaleLowerCase("ru-RU");
|
|
5899
|
+
if (text === "schools" || /школ|лице|гимнази/.test(text)) return "schools";
|
|
5900
|
+
if (text === "kindergartens" || /сад|детсад|садик/.test(text)) return "kindergartens";
|
|
5901
|
+
return "all";
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
function inferGeoDataset(text) {
|
|
5905
|
+
const normalized = String(text || "").toLocaleLowerCase("ru-RU");
|
|
5906
|
+
if (/(сад|детсад|садик)/iu.test(normalized)) return "kindergartens";
|
|
5907
|
+
if (/(школ|лице|гимнази)/iu.test(normalized)) return "schools";
|
|
5908
|
+
return "all";
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
function extractAddressForNearby(question) {
|
|
5912
|
+
const text = String(question || "").trim();
|
|
5913
|
+
const match = text.match(/(?:рядом с|рядом|около|возле|поблизости от|живу на|мой адрес|по адресу)\s+(.+)/iu);
|
|
5914
|
+
if (match?.[1]) return cleanupGeoAddress(match[1].split(/[,;]\s*(?:какие|что|где|кто|дай|покажи)/iu)[0]);
|
|
5915
|
+
return cleanupGeoAddress(text);
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
function extractDistancePair(question) {
|
|
5919
|
+
const text = String(question || "").trim();
|
|
5920
|
+
const explicit = {
|
|
5921
|
+
from: text.match(/(?:от|from)\s+(.+?)\s+(?:до|к|to)\s+(.+)/iu)?.[1],
|
|
5922
|
+
to: text.match(/(?:от|from)\s+(.+?)\s+(?:до|к|to)\s+(.+)/iu)?.[2],
|
|
5923
|
+
};
|
|
5924
|
+
if (explicit.from && explicit.to) return { from: cleanupGeoAddress(explicit.from), to: cleanupGeoAddress(explicit.to) };
|
|
5925
|
+
const parts = text.split(/\s+(?:и|до|->)\s+/iu).map(cleanupGeoAddress).filter(Boolean);
|
|
5926
|
+
return { from: parts[0] || "", to: parts[1] || "" };
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
function cleanupGeoObjectQuery(text) {
|
|
5930
|
+
return cleanupGeoAddress(String(text || "")
|
|
5931
|
+
.replace(/^(?:где находится|как пройти|как добраться|покажи|дай|открой|найди|ссылку на карту|ссылка на карту)\s+/iu, "")
|
|
5932
|
+
.replace(/\b(?:на карте|карту|карта|рядом|что рядом|ориентиры?)\b/giu, ""));
|
|
5933
|
+
}
|
|
5934
|
+
|
|
5935
|
+
function cleanupGeoAddress(text) {
|
|
5936
|
+
return String(text || "")
|
|
5937
|
+
.replace(/[?.!]+$/u, "")
|
|
5938
|
+
.replace(/(^|\s)улице(?=\s|$)/giu, "$1улица")
|
|
5939
|
+
.replace(/\b(?:какие|какой|какая|есть|ближайшие|ближайший|школы|школа|детские сады|детский сад|садики|садик|объекты|городские|учреждения|рядом|поблизости)\b/giu, " ")
|
|
5940
|
+
.replace(/\s+/g, " ")
|
|
5941
|
+
.trim();
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
function normalizeAddressForGeocoder(address) {
|
|
5945
|
+
const source = String(address || "").trim();
|
|
5946
|
+
if (!source) return source;
|
|
5947
|
+
let text = source
|
|
5948
|
+
.replace(/^\s*\d{6},?\s*/u, "")
|
|
5949
|
+
.replace(/\bРоссия,\s*/iu, "")
|
|
5950
|
+
.replace(/\bРоссийская Федерация,\s*/iu, "")
|
|
5951
|
+
.replace(/\bРеспублика Марий Эл,\s*/iu, "")
|
|
5952
|
+
.replace(/\bгородской округ\s+город\s+Йошкар-Ола,\s*/iu, "")
|
|
5953
|
+
.replace(/\bГОРОД ЙОШКАР-ОЛА,\s*/iu, "")
|
|
5954
|
+
.replace(/\bЙошкар-Ола\s+г,\s*/iu, "")
|
|
5955
|
+
.replace(/\bгород\s+Йошкар-Ола,\s*/iu, "Йошкар-Ола, ")
|
|
5956
|
+
.replace(/\bсело\s+Сем[её]новка,\s*/iu, "Йошкар-Ола, Семёновка, ")
|
|
5957
|
+
.replace(/\bдом\s+/giu, "д. ")
|
|
5958
|
+
.replace(/\bулица\s+/giu, "ул. ")
|
|
5959
|
+
.replace(/([А-ЯЁ])\.(?=[А-ЯЁ])/gu, "$1. ")
|
|
5960
|
+
.replace(/\s+/g, " ")
|
|
5961
|
+
.replace(/,\s*,/g, ",")
|
|
5962
|
+
.trim();
|
|
5963
|
+
if (!/(йошкар|сем[её]новк)/iu.test(text)) text = `Йошкар-Ола, ${text}`;
|
|
5964
|
+
return text;
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
function normalizeGeoText(text) {
|
|
5968
|
+
return String(text || "").toLocaleLowerCase("ru-RU").replace(/ё/g, "е").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5495
5971
|
async function getYandexGeocoderKey() {
|
|
5496
5972
|
if (process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY) {
|
|
5497
5973
|
return process.env.YANDEX_GEOCODER_API_KEY || process.env.YANDEX_MAPS_API_KEY;
|
|
@@ -6912,6 +7388,20 @@ async function aiAsk(args, context = {}) {
|
|
|
6912
7388
|
const historyEnabled = !options.bare && !options["no-history"] && isFeatureEnabled("sqlite-history");
|
|
6913
7389
|
const sessionId = historyEnabled && isFeatureEnabled("sessions") ? ensureSessionForAsk(options, providerConfig, question) : null;
|
|
6914
7390
|
const history = context.history || (sessionId ? getSessionAiHistory(sessionId) : []);
|
|
7391
|
+
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
7392
|
+
if (geoAnswer) {
|
|
7393
|
+
if (historyEnabled) {
|
|
7394
|
+
recordAskHistory({ question, answer: geoAnswer, providerConfig, dataContext, error: "", sessionId });
|
|
7395
|
+
appendSessionExchange(sessionId, question, geoAnswer, dataContext, "");
|
|
7396
|
+
}
|
|
7397
|
+
emitEvent(options, "answer", { length: geoAnswer.length, sessionId, direct: true, geo: true });
|
|
7398
|
+
if (options.output) {
|
|
7399
|
+
await assertPermission("writeFiles");
|
|
7400
|
+
await writeFile(options.output, geoAnswer, "utf8");
|
|
7401
|
+
}
|
|
7402
|
+
if (!options.quiet) console.log(geoAnswer);
|
|
7403
|
+
return geoAnswer;
|
|
7404
|
+
}
|
|
6915
7405
|
const directAnswer = await buildDirectDataAnswer(question, dataContext);
|
|
6916
7406
|
if (directAnswer) {
|
|
6917
7407
|
if (historyEnabled) {
|
|
@@ -7281,6 +7771,11 @@ async function localToolAsk(question, providerConfig, options) {
|
|
|
7281
7771
|
if (!options.quiet) console.log(casualAnswer);
|
|
7282
7772
|
return casualAnswer;
|
|
7283
7773
|
}
|
|
7774
|
+
const geoAnswer = await buildGeoDirectAnswer(question);
|
|
7775
|
+
if (geoAnswer) {
|
|
7776
|
+
if (!options.quiet) console.log(geoAnswer);
|
|
7777
|
+
return geoAnswer;
|
|
7778
|
+
}
|
|
7284
7779
|
await ensureLocalData();
|
|
7285
7780
|
const personRoleAnswer = buildPersonRoleDirectAnswer(question);
|
|
7286
7781
|
if (personRoleAnswer) {
|
|
@@ -7754,9 +8249,9 @@ function extractEntityNumberFromQuestion(question, layer) {
|
|
|
7754
8249
|
const isKindergarten = layer === "kindergartens";
|
|
7755
8250
|
const isSchool = layer === "schools";
|
|
7756
8251
|
const patterns = isKindergarten
|
|
7757
|
-
? [/(
|
|
8252
|
+
? [/(?:детск[\p{L}\p{N}_-]*\s+сад[\p{L}\p{N}_-]*|детсад[\p{L}\p{N}_-]*|сад[\p{L}\p{N}_-]*)\s*(?:№|номер|n)?\s*(\d{1,4})/iu, /№\s*(\d{1,4})/iu]
|
|
7758
8253
|
: isSchool
|
|
7759
|
-
? [/(
|
|
8254
|
+
? [/(?:школ[\p{L}\p{N}_-]*|сош|гимнази[\p{L}\p{N}_-]*|лице[\p{L}\p{N}_-]*)\s*(?:№|номер|n)?\s*(\d{1,4})/iu, /№\s*(\d{1,4})/iu]
|
|
7760
8255
|
: [/№\s*(\d{1,4})/iu];
|
|
7761
8256
|
for (const pattern of patterns) {
|
|
7762
8257
|
const match = text.match(pattern);
|
|
@@ -9567,7 +10062,7 @@ function parseOptions(args) {
|
|
|
9567
10062
|
} else if (arg === "--check" || arg === "--upgrade-node") {
|
|
9568
10063
|
result.check = true;
|
|
9569
10064
|
result[arg.slice(2)] = true;
|
|
9570
|
-
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file") {
|
|
10065
|
+
} else if (arg === "--limit" || arg === "--offset" || arg === "--search" || arg === "--replace" || arg === "--text" || arg === "--path" || arg === "--depth" || arg === "--max-bytes" || arg === "--query" || arg === "--where" || arg === "--columns" || arg === "--inn" || arg === "--model" || arg === "--provider" || arg === "--profile" || arg === "--name" || arg === "--source" || arg === "--command" || arg === "--prompt" || arg === "--description" || arg === "--base-url" || arg === "--repo" || arg === "--model-dir" || arg === "--sandbox" || arg === "--approval" || arg === "--cwd" || arg === "--codex-profile" || arg === "--format" || arg === "--output" || arg === "--schema" || arg === "--session" || arg === "--temperature" || arg === "--config" || arg === "--dataset" || arg === "--save" || arg === "--reasoning" || arg === "--agent" || arg === "--scope" || arg === "--selector" || arg === "--url" || arg === "--timeout" || arg === "--wait" || arg === "--viewport" || arg === "--press" || arg === "--script" || arg === "--auth-url" || arg === "--token-url" || arg === "--userinfo-url" || arg === "--client-id" || arg === "--client-secret" || arg === "--redirect-host" || arg === "--redirect-port" || arg === "--redirect-path" || arg === "--debug-file" || arg === "--from" || arg === "--to" || arg === "--radius" || arg === "--address") {
|
|
9571
10066
|
result[arg.slice(2)] = args[index + 1];
|
|
9572
10067
|
index += 1;
|
|
9573
10068
|
} else {
|
|
@@ -9816,6 +10311,7 @@ function selectSkillsForPrompt(config, question = "", options = {}) {
|
|
|
9816
10311
|
const normalized = String(question || "").toLocaleLowerCase("ru-RU");
|
|
9817
10312
|
if (enabled.has("local-model")) selected.add("local-model");
|
|
9818
10313
|
if (enabled.has("open-data") && shouldUseDataContext(question, options)) selected.add("open-data");
|
|
10314
|
+
if (enabled.has("geo") && isGeoQuestion(normalized)) selected.add("geo");
|
|
9819
10315
|
if (enabled.has("reports") && /(отчет|отчёт|выгруз|csv|xlsx|качество|провер)/iu.test(normalized)) selected.add("reports");
|
|
9820
10316
|
if (enabled.has("local-files") && (options.files || /(файл|папк|readme|документ|архив)/iu.test(normalized))) selected.add("local-files");
|
|
9821
10317
|
if (enabled.has("browser-agent") && /(браузер|сайт|страниц|url|https?:\/\/)/iu.test(normalized)) selected.add("browser-agent");
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
9. После создания ключ появится в списке ключей API Геокодера.
|
|
24
24
|
10. Скопируйте значение ключа. Это значение будет `YANDEX_GEOCODER_API_KEY`.
|
|
25
25
|
|
|
26
|
-
Важно: новый ключ может начать работать не сразу. В документации Яндекса указано, что активация ключа может занять до 15 минут. Если CLI пишет `Invalid api key` сразу после создания ключа, подождите и повторите
|
|
26
|
+
Важно: новый ключ может начать работать не сразу. В документации Яндекса указано, что активация ключа может занять до 15 минут, на практике лучше спокойно подождать 15-30 минут. Если CLI пишет `Invalid api key` сразу после создания ключа, это не обязательно ошибка: подождите и повторите проверку командой `iola geo key doctor`.
|
|
27
27
|
|
|
28
28
|
## Сохранение в CLI
|
|
29
29
|
|
|
@@ -33,6 +33,12 @@ iola geo key set yandex
|
|
|
33
33
|
iola geo key status
|
|
34
34
|
iola geo key doctor
|
|
35
35
|
iola geo geocode "Йошкар-Ола, улица Петрова, 15"
|
|
36
|
+
iola geo nearby "Йошкар-Ола, улица Петрова, 15" --dataset all --limit 5
|
|
37
|
+
iola geo distance --from "Петрова 15" --to "школа 7"
|
|
38
|
+
iola geo map-link "школа 7"
|
|
39
|
+
iola geo resolve "садик золотой петушок"
|
|
40
|
+
iola geo route-context "школа 7"
|
|
41
|
+
iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
Локальная БД:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Скиллы для жителей
|
|
2
2
|
|
|
3
|
-
Эта страница фиксирует пользовательские skills, которые
|
|
3
|
+
Эта страница фиксирует пользовательские skills, которые реализованы и развиваются в `iola-cli`.
|
|
4
4
|
|
|
5
5
|
Цель этих skills - помогать жителю города в бытовых вопросах: где находится объект, что рядом, какой объект ближе, как открыть место на карте. Это не административный аудит данных.
|
|
6
6
|
|
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
Для geo skills нужен ключ `Yandex Geocoder API`.
|
|
10
10
|
|
|
11
|
+
Первый рабочий контур подключен к слоям `schools` и `kindergartens`.
|
|
12
|
+
|
|
13
|
+
Команды:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
iola geo nearby "Йошкар-Ола, улица Петрова, 15" --dataset all --limit 5
|
|
17
|
+
iola geo distance --from "Петрова 15" --to "школа 7"
|
|
18
|
+
iola geo map-link "школа 7"
|
|
19
|
+
iola geo resolve "садик золотой петушок"
|
|
20
|
+
iola geo route-context "школа 7"
|
|
21
|
+
iola geo services "Йошкар-Ола, улица Петрова, 15"
|
|
22
|
+
```
|
|
23
|
+
|
|
11
24
|
### nearby
|
|
12
25
|
|
|
13
26
|
Находит городские объекты рядом с адресом пользователя.
|
|
@@ -18,7 +31,7 @@
|
|
|
18
31
|
- `какие школы ближе к улице Машиностроителей`;
|
|
19
32
|
- `что из городских учреждений рядом с домом`.
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
Сейчас работает по слоям `schools` и `kindergartens`. Позже можно подключить МФЦ, поликлиники, остановки, спортобъекты и учреждения культуры.
|
|
22
35
|
|
|
23
36
|
### route-context
|
|
24
37
|
|
|
@@ -81,17 +94,8 @@
|
|
|
81
94
|
|
|
82
95
|
Это важно, чтобы не путать объекты с одинаковыми номерами или похожими названиями.
|
|
83
96
|
|
|
84
|
-
##
|
|
85
|
-
|
|
86
|
-
Первый этап:
|
|
87
|
-
|
|
88
|
-
1. `nearby`
|
|
89
|
-
2. `place-resolver`
|
|
90
|
-
3. `map-link`
|
|
91
|
-
|
|
92
|
-
Второй этап:
|
|
97
|
+
## Ограничения
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
4. `address-to-services`
|
|
99
|
+
- Расстояние считается по прямой между координатами. Это не автомобильный и не пешеходный маршрут.
|
|
100
|
+
- CLI не должен подставлять похожий объект из другого населенного пункта. Если слой не содержит нужный объект, ответ должен прямо говорить, что данных недостаточно.
|
|
101
|
+
- При первом поиске рядом CLI геокодирует объекты слоя. Повторные запросы в той же сессии работают быстрее за счет памяти процесса.
|