Geocode neighborhood names to an approximate location (no source coords)
Many Medjobs/Telegram ads name a Tehran neighborhood («ونک», «تهرانپارس»…) but carry no coordinates. New TehranGeo geocoder maps ~45 neighborhood names to a rough center; Publish falls back to it (from the resolved district / AI district / area note) when the source ad has no point. Shown via the existing «محدودهٔ تقریبی» circle + disclaimer — never a precise pin. Tehran-only; extends the existing approx-coords feature so non-Divar listings can show a map too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -307,6 +307,14 @@ public class IngestionService
|
||||
?? cities.FirstOrDefault(c => c.IsActive) ?? cities.First();
|
||||
var district = districts.FirstOrDefault(x => x.Name == districtName && x.CityId == city.Id);
|
||||
|
||||
// Approx. coords for the map: the source ad's point (Divar) when present; otherwise, for a
|
||||
// Tehran ad that only NAMES a neighborhood (Medjobs/Telegram), geocode that name to a rough
|
||||
// center. Shown as a «محدودهٔ تقریبی» circle, never a precise pin.
|
||||
double? appLat = raw.Lat, appLng = raw.Lng;
|
||||
if (appLat is null && city.Name == "تهران"
|
||||
&& TehranGeo.Locate(district?.Name, districtName, parsed.AreaNote) is { } g)
|
||||
{ appLat = g.lat; appLng = g.lng; }
|
||||
|
||||
var kindStr = (d?.Kind ?? parsed.Kind.ToString()).ToLowerInvariant();
|
||||
|
||||
// «آماده به کار» — a worker offering themselves. No facility involved.
|
||||
@@ -334,7 +342,7 @@ public class IngestionService
|
||||
Phone = !string.IsNullOrWhiteSpace(d?.Phone) ? d!.Phone!.Trim() : parsed.Phone,
|
||||
Description = raw.RawText,
|
||||
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
|
||||
Lat = raw.Lat, Lng = raw.Lng, // approx. area from the source ad (Divar)
|
||||
Lat = appLat, Lng = appLng, // source point (Divar) or geocoded neighborhood center
|
||||
Contacts = BuildContacts(d, parsed),
|
||||
Tags = BuildTags(parsed, d, role, city, extraRoleTags),
|
||||
});
|
||||
@@ -382,7 +390,7 @@ public class IngestionService
|
||||
SalaryMin = parsed.PayAmount,
|
||||
Description = raw.RawText, Status = ShiftStatus.Open, Source = ShiftSource.Aggregated,
|
||||
SourceUrl = raw.SourceUrl,
|
||||
Lat = raw.Lat, Lng = raw.Lng, // approx. area from the source ad (Divar)
|
||||
Lat = appLat, Lng = appLng, // source point (Divar) or geocoded neighborhood center
|
||||
Contacts = BuildContacts(d, parsed), // the ad's OWN number(s) — fresh per listing
|
||||
});
|
||||
}
|
||||
@@ -401,7 +409,7 @@ public class IngestionService
|
||||
: parsed.PayAmount is null ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = parsed.PayAmount, SharePercent = parsed.SharePercent,
|
||||
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
|
||||
Lat = raw.Lat, Lng = raw.Lng, // approx. area from the source ad (Divar)
|
||||
Lat = appLat, Lng = appLng, // source point (Divar) or geocoded neighborhood center
|
||||
Contacts = BuildContacts(d, parsed), // the ad's OWN number(s) — fresh per listing
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace JobsMedical.Web.Services.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Coarse neighborhood → APPROXIMATE center geocoder for Tehran. Many ads (Medjobs/Telegram) name a
|
||||
/// neighborhood but carry no coordinates; this lets us show an approximate-area circle from the name
|
||||
/// alone. Centers are deliberately rough (the UI always labels them «محدودهٔ تقریبی»), never an
|
||||
/// address. Extend the table freely — order doesn't matter, matching is name-normalized + substring.
|
||||
/// </summary>
|
||||
public static class TehranGeo
|
||||
{
|
||||
private static readonly (string Name, double Lat, double Lng)[] Raw =
|
||||
{
|
||||
("سعادتآباد", 35.7872, 51.3760), ("شهرک غرب", 35.7570, 51.3680), ("نارمک", 35.7448, 51.5085),
|
||||
("تهرانپارس", 35.7350, 51.5400), ("ونک", 35.7560, 51.4100), ("تجریش", 35.8040, 51.4340),
|
||||
("ولیعصر", 35.7986, 51.4087), ("پارکوی", 35.7986, 51.4087), ("گیشا", 35.7400, 51.3880),
|
||||
("برج میلاد", 35.7448, 51.3753), ("پاسداران", 35.7890, 51.4560), ("میرداماد", 35.7600, 51.4300),
|
||||
("جردن", 35.7700, 51.4180), ("آفریقا", 35.7700, 51.4180), ("ولنجک", 35.8080, 51.4080),
|
||||
("نیاوران", 35.8170, 51.4700), ("زعفرانیه", 35.8100, 51.4200), ("الهیه", 35.7900, 51.4320),
|
||||
("قیطریه", 35.7950, 51.4450), ("فرمانیه", 35.8000, 51.4700), ("دروس", 35.7850, 51.4500),
|
||||
("یوسفآباد", 35.7370, 51.4050), ("امیرآباد", 35.7260, 51.3920), ("انقلاب", 35.7010, 51.3940),
|
||||
("صادقیه", 35.7150, 51.3450), ("پونک", 35.7620, 51.3300), ("جنتآباد", 35.7600, 51.3100),
|
||||
("اکباتان", 35.7150, 51.3100), ("ستارخان", 35.7200, 51.3550), ("مرزداران", 35.7400, 51.3500),
|
||||
("نازیآباد", 35.6400, 51.4080), ("یافتآباد", 35.6600, 51.3500), ("شهرری", 35.5850, 51.4350),
|
||||
("پیروزی", 35.7000, 51.4800), ("رسالت", 35.7450, 51.5000), ("حکیمیه", 35.7450, 51.5800),
|
||||
("تهرانسر", 35.7100, 51.2500), ("شریعتی", 35.7600, 51.4400), ("سهروردی", 35.7300, 51.4300),
|
||||
("آزادی", 35.7000, 51.3600), ("جمهوری", 35.6960, 51.3920), ("هفت تیر", 35.7250, 51.4230),
|
||||
("ولیعصر پایین", 35.7100, 51.4070), ("نواب", 35.6850, 51.3750), ("سعدی", 35.6900, 51.4250),
|
||||
};
|
||||
|
||||
// Built once: normalized name → center. Insertion order kept for the substring pass.
|
||||
private static readonly List<(string Key, double Lat, double Lng)> Map =
|
||||
Raw.Select(x => (Norm(x.Name), x.Lat, x.Lng)).ToList();
|
||||
|
||||
/// <summary>First of the given names that maps to a known Tehran neighborhood center (exact, then
|
||||
/// substring — «میدان ونک» → «ونک»). Returns null when nothing matches.</summary>
|
||||
public static (double lat, double lng)? Locate(params string?[] names)
|
||||
{
|
||||
foreach (var raw in names)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) continue;
|
||||
var n = Norm(raw);
|
||||
foreach (var m in Map) if (m.Key == n) return (m.Lat, m.Lng); // exact
|
||||
foreach (var m in Map) if (n.Contains(m.Key)) return (m.Lat, m.Lng); // «… ونک …»
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string Norm(string s) => Regex.Replace(
|
||||
s.Replace('ي', 'ی').Replace('ك', 'ک').Replace('', ' ').Trim(), @"\s+", " ").ToLowerInvariant();
|
||||
}
|
||||
Reference in New Issue
Block a user