module SunbusStopSearch
SunbusStopSearch — fuzzy stop name and suburb ranking for GTFS stop lists. Public: search, search_variants, normalize_text, query_tokens, rank_by_name, rank_by_suburb Depends: SunbusConfig Tests: test/lib/sunbus_stop_search_test.rb
Constants
- ABBREVIATIONS
- STOP_WORDS
Public Instance Methods
Source
# File lib/sunbus/stop_search.rb, line 40 def distance_km(lat1, lon1, lat2, lon2) radius = 6371.0 dlat = (lat2 - lat1) * Math::PI / 180 dlon = (lon2 - lon1) * Math::PI / 180 lat1_rad = lat1 * Math::PI / 180 lat2_rad = lat2 * Math::PI / 180 a = Math.sin(dlat / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin(dlon / 2)**2 2 * radius * Math.asin(Math.sqrt(a)) end
Source
# File lib/sunbus/stop_search.rb, line 36 def expand_tokens(tokens) tokens.flat_map { |token| ABBREVIATIONS[token] ? [token, ABBREVIATIONS[token]] : [token] } end
Source
# File lib/sunbus/stop_search.rb, line 116 def merge_unique(first, second, limit:) seen = Set.new merged = [] (first + second).each do |stop| key = stop[:stop_id] next if seen.include?(key) seen.add(key) merged << stop break if merged.length >= limit end merged end
Source
# File lib/sunbus/stop_search.rb, line 62 def name_score(stop, query) name = normalize_text(stop[:name]) needle = normalize_text(query) return nil if needle.empty? if numeric_query?(query) code = stop[:stop_code].to_s id = stop[:stop_id].to_s return 0 if code == needle || id == needle return 1 if code.end_with?(needle) || id.end_with?(needle) return nil end return 0 if name == needle return 1 if name.start_with?(needle) return 2 if name.include?(needle) tokens = expand_tokens(query_tokens(query)) return nil if tokens.empty? return 3 if tokens.all? { |token| name.include?(token) } nil end
Source
# File lib/sunbus/stop_search.rb, line 24 def normalize_text(text) text.to_s.downcase .gsub(/['']/, "") .gsub(/[^a-z0-9\s]/, " ") .squeeze(" ") .strip end
Source
# File lib/sunbus/stop_search.rb, line 58 def numeric_query?(query) query.to_s.strip.match?(/\A\d+\z/) end
Source
# File lib/sunbus/stop_search.rb, line 32 def query_tokens(query) normalize_text(query).split.reject { |token| token.length < 2 || STOP_WORDS.include?(token) } end
Source
# File lib/sunbus/stop_search.rb, line 98 def rank_by_name(stops, query) stops.filter_map do |stop| score = name_score(stop, query) next unless score [score, stop] end.sort_by { |score, _| score }.map(&:last) end
Source
# File lib/sunbus/stop_search.rb, line 107 def rank_by_suburb(stops, suburb) stops.filter_map do |stop| distance = suburb_geo_score(stop, suburb) next unless distance [distance, stop] end.sort_by { |distance, _| distance }.map(&:last) end
Source
# File lib/sunbus/stop_search.rb, line 130 def search(stops, query, region: SunbusConfig::DEFAULT_REGION, limit: SunbusConfig::MAX_STOPS) SunbusAliases.variants(query).each do |variant| matches = search_variants(stops, variant, region: region, limit: limit) return matches unless matches.empty? end [] end
Source
# File lib/sunbus/stop_search.rb, line 139 def search_variants(stops, query, region:, limit:) needle = query.to_s.strip return [] if needle.empty? name_ranked = rank_by_name(stops, needle) suburb = SunbusSuburbs.resolve(needle, region: region) if suburb geo_ranked = rank_by_suburb(stops, suburb) return merge_unique(name_ranked, geo_ranked, limit: limit) end name_ranked.first(limit) end
Source
# File lib/sunbus/stop_search.rb, line 50 def stop_lat_lon(stop) lat = stop[:lat] lon = stop[:lon] return nil unless lat && lon [lat.to_f, lon.to_f] end
Source
# File lib/sunbus/stop_search.rb, line 88 def suburb_geo_score(stop, suburb) coords = stop_lat_lon(stop) return nil unless coords distance = distance_km(suburb[:lat], suburb[:lon], coords[0], coords[1]) return nil if distance > suburb[:radius_km] distance end