Bring back profile subsumption elimination for overpass queries

This commit is contained in:
stopflock
2026-07-15 21:20:44 -05:00
parent f8632b6a06
commit 38eee8d125
4 changed files with 218 additions and 16 deletions
+85 -2
View File
@@ -126,9 +126,24 @@ class OverpassService {
return ErrorDisposition.retry;
}
/// Build Overpass QL query for given bounds and profiles
/// Build Overpass QL query for given bounds and profiles.
///
/// Profiles are deduplicated first: if one profile's non-empty tags are a
/// subset of another's (with identical values), the broader profile's query
/// clause already returns every node the narrower one would, so the
/// narrower clause is redundant and can be dropped. This directly reduces
/// query size/complexity without changing the result set — client-side
/// filtering (matching returned nodes against the full profile list) still
/// happens independently, so nothing is lost.
String _buildQuery(LatLngBounds bounds, List<NodeProfile> profiles) {
final nodeClauses = profiles.map((profile) {
final profilesToQuery = _deduplicateProfilesForQuery(profiles);
if (profilesToQuery.length < profiles.length) {
debugPrint(
'[OverpassService] Deduplicated ${profiles.length} profiles to ${profilesToQuery.length} for query efficiency');
}
final nodeClauses = profilesToQuery.map((profile) {
// Convert profile tags to Overpass filter format, excluding empty values
final tagFilters = profile.tags.entries
.where((entry) => entry.value.trim().isNotEmpty)
@@ -149,6 +164,74 @@ out ids;
''';
}
/// Deduplicate profiles for Overpass queries by removing profiles that are
/// subsumed by others. A profile A subsumes profile B if all of A's
/// non-empty tags exist in B with identical values — meaning every node
/// matching B's filter also matches A's filter, so B's clause is redundant.
List<NodeProfile> _deduplicateProfilesForQuery(List<NodeProfile> profiles) {
if (profiles.length <= 1) return profiles;
final result = <NodeProfile>[];
for (final candidate in profiles) {
// Skip profiles that only have empty tags - they would match
// everything and shouldn't be treated as subsuming or subsumable.
final candidateNonEmptyTags = candidate.tags.entries
.where((entry) => entry.value.trim().isNotEmpty)
.toList();
if (candidateNonEmptyTags.isEmpty) continue;
// Check if any existing profile in our result subsumes this candidate
bool isSubsumed = false;
for (final existing in result) {
if (_profileSubsumes(existing, candidate)) {
isSubsumed = true;
break;
}
}
if (!isSubsumed) {
// This candidate is not subsumed, so add it. But first, remove any
// existing profiles that this candidate subsumes (candidate is
// broader, so those clauses are now redundant).
result.removeWhere((existing) => _profileSubsumes(candidate, existing));
result.add(candidate);
}
}
// Safety check: if dedup removed everything (e.g. all-empty-tag
// profiles), fall back to the original list rather than sending an
// empty query.
return result.isNotEmpty ? result : profiles;
}
/// Check if [broaderProfile] subsumes [specificProfile]: true if all
/// non-empty tags in [broaderProfile] exist in [specificProfile] with
/// identical values.
bool _profileSubsumes(NodeProfile broaderProfile, NodeProfile specificProfile) {
final broaderTags = Map.fromEntries(
broaderProfile.tags.entries.where((entry) => entry.value.trim().isNotEmpty),
);
final specificTags = Map.fromEntries(
specificProfile.tags.entries.where((entry) => entry.value.trim().isNotEmpty),
);
// If broader has no non-empty tags, it doesn't subsume anything (it
// would match everything, which isn't a meaningful "broader filter").
if (broaderTags.isEmpty) return false;
// If broader has more non-empty tags than specific, it can't subsume it.
if (broaderTags.length > specificTags.length) return false;
for (final entry in broaderTags.entries) {
if (specificTags[entry.key] != entry.value) return false;
}
return true;
}
/// Parse Overpass JSON response into OsmNode objects
List<OsmNode> _parseResponse(String responseBody) {
final data = jsonDecode(responseBody) as Map<String, dynamic>;