using System.Collections.Generic;
using System.Text.RegularExpressions;
private static readonly Dictionary<char, char> TurkishCharMap = new Dictionary<char, char>
{ 'ı', 'I' }, { 'i', 'I' }, { 'İ', 'I' }, { 'I', 'I' },
{ 'ğ', 'G' }, { 'g', 'G' }, { 'Ğ', 'G' }, { 'G', 'G' },
{ 'ü', 'U' }, { 'u', 'U' }, { 'Ü', 'U' }, { 'U', 'U' },
{ 'ş', 'S' }, { 's', 'S' }, { 'Ş', 'S' }, { 'S', 'S' },
{ 'ö', 'O' }, { 'o', 'O' }, { 'Ö', 'O' }, { 'O', 'O' },
{ 'ç', 'C' }, { 'c', 'C' }, { 'Ç', 'C' }, { 'C', 'C' }
private static readonly string[] TurkishCities = {
"Adana", "Adıyaman", "Afyonkarahisar", "Ağrı", "Aksaray", "Amasya", "Ankara", "Antalya", "Ardahan",
"Artvin", "Aydın", "Balıkesir", "Bartın", "Batman", "Bayburt", "Bilecik", "Bingöl", "Bitlis",
"Bolu", "Burdur", "Bursa", "Çanakkale", "Çankırı", "Çorum", "Denizli", "Diyarbakır", "Düzce",
"Edirne", "Elazığ", "Erzincan", "Erzurum", "Eskişehir", "Gaziantep", "Giresun", "Gümüşhane",
"Hakkari", "Hatay", "Iğdır", "Isparta", "İstanbul", "İzmir", "Kahramanmaraş", "Karabük", "Karaman",
"Kars", "Kastamonu", "Kayseri", "Kilis", "Kırıkkale", "Kırklareli", "Kırşehir", "Kocaeli",
"Konya", "Kütahya", "Malatya", "Manisa", "Mardin", "Mersin", "Muğla", "Muş", "Nevşehir",
"Niğde", "Ordu", "Osmaniye", "Rize", "Sakarya", "Samsun", "Şanlıurfa", "Siirt", "Sinop",
"Şırnak", "Sivas", "Tekirdağ", "Tokat", "Trabzon", "Tunceli", "Uşak", "Van", "Yalova",
private static readonly Dictionary<string, string> NormalizedCities = TurkishCities
.GroupBy(city => city.NormalizeTurkishToAscii(), StringComparer.OrdinalIgnoreCase)
StringComparer.OrdinalIgnoreCase
string[] inputs = { "Aydinnnnnnnnnnnnnnnnnnnnnnnnnnnnn", "mula","ıSTANBUL 123", "istan!", "Bur @", "Ank 456", "İzmir#", "Konya 789", "Ist an bul", "ıSTANBUL", "ISTANBUL",
"istanbul", "İstanBUL", "istan", " ıstan", "Bur", "Ank", "İzmir", "Konya", "ığdır", "İGDır", "İĞDIR" };
const double SIMILARITY_THRESHOLD = 0.65;
foreach (var input in inputs)
var isMatch = NormalizedCities.TryMatchWithSimilarity(input, SIMILARITY_THRESHOLD, out var city);
Console.WriteLine($"{input} => {city} (Normalized: {city.NormalizeTurkishToAscii()})");
Console.WriteLine($"{input} => no match found (similarity < {SIMILARITY_THRESHOLD:P0})");
public static partial class StringExtensions
private static readonly Dictionary<char, char> TurkishCharMap = new()
{ 'ı', 'I' }, { 'i', 'I' }, { 'İ', 'I' }, { 'I', 'I' },
{ 'ğ', 'G' }, { 'g', 'G' }, { 'Ğ', 'G' }, { 'G', 'G' },
{ 'ü', 'U' }, { 'u', 'U' }, { 'Ü', 'U' }, { 'U', 'U' },
{ 'ş', 'S' }, { 's', 'S' }, { 'Ş', 'S' }, { 'S', 'S' },
{ 'ö', 'O' }, { 'o', 'O' }, { 'Ö', 'O' }, { 'O', 'O' },
{ 'ç', 'C' }, { 'c', 'C' }, { 'Ç', 'C' }, { 'C', 'C' }
public static string NormalizeTurkishToAscii(this string input)
if (string.IsNullOrWhiteSpace(input))
var cleanedInput = Regex.Replace(input, "[^a-zA-ZğüşıöçĞÜŞİÖÇ]","");
if (string.IsNullOrWhiteSpace(cleanedInput))
var normalized = new StringBuilder(cleanedInput.Length);
foreach (var c in cleanedInput)
normalized.Append(TurkishCharMap.TryGetValue(c, out var value) ? value : char.ToUpperInvariant(c));
return normalized.ToString();
public static bool TryMatch<T>(this Dictionary<string, T> normalizedCities, string input, out T? value)
var normalizedInput = NormalizeTurkishToAscii(input);
if (string.IsNullOrWhiteSpace(normalizedInput))
foreach (var pair in normalizedCities)
if (string.Equals(pair.Key, normalizedInput, StringComparison.OrdinalIgnoreCase))
T matchedValue = default;
foreach (var pair in normalizedCities)
if (pair.Key.StartsWith(normalizedInput, StringComparison.OrdinalIgnoreCase))
matchedValue = pair.Value;
private static string LimitRepeatingChars(string input, int maxRepeat = 3)
if (string.IsNullOrEmpty(input) || maxRepeat < 1)
var result = new StringBuilder();
char lastChar = input[0];
for (int i = 1; i < input.Length; i++)
if (input[i] == lastChar)
if (repeatCount <= maxRepeat)
return result.ToString();
public static bool TryMatchWithSimilarity<T>(this Dictionary<string, T> normalizedCities, string input, double similarityThreshold, out T? value)
var normalizedInput = NormalizeTurkishToAscii(input);
if (string.IsNullOrWhiteSpace(normalizedInput))
normalizedInput = LimitRepeatingChars(normalizedInput);
if (TryMatch(normalizedCities, input, out value))
var bestMatch = normalizedCities
Similarity = CalculateSimilarity(pair.Key, normalizedInput)
.OrderByDescending(x => x.Similarity)
if (bestMatch != null && bestMatch.Similarity >= similarityThreshold)
private static double CalculateSimilarity(string source, string target)
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(target)) return 0;
if (source == target) return 1;
int distance = LevenshteinDistance(source, target);
int maxLength = Math.Max(source.Length, target.Length);
return 1 - ((double)distance / maxLength);
private static int LevenshteinDistance(string source, string target)
var matrix = new int[source.Length + 1, target.Length + 1];
for (int i = 0; i <= source.Length; i++)
for (int j = 0; j <= target.Length; j++)
for (int i = 1; i <= source.Length; i++)
for (int j = 1; j <= target.Length; j++)
int cost = (source[i - 1] == target[j - 1]) ? 0 : 1;
Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1),
matrix[i - 1, j - 1] + cost
return matrix[source.Length, target.Length];