searchRooms method

Future<List<CampusMapRoom>> searchRooms(
  1. String query, {
  2. Map<String, String> buildingNames = const {},
})

Searches room names, numbers, and building-qualified classroom references.

The complete metadata index loads on first search and expires after 14 days. Existing complete data remains usable offline. Without a complete index, network failure propagates so a partial match cannot cause a wrong unique navigation. Exact matches take precedence over substring matches. buildingNames optionally maps building codes to additional display-name aliases. Original server names remain searchable and are returned intact. Only overview buildings with an advertised first floor are searchable; their rooms on other floors remain eligible.

Implementation

Future<List<CampusMapRoom>> searchRooms(
  String query, {
  Map<String, String> buildingNames = const {},
}) async {
  final normalized = normalizeCampusMapQuery(query);
  if (normalized.isEmpty) return [];
  var stamp = (await _sync())?.indexFetchedAt;
  if (stamp == null || _now().difference(stamp) >= _cacheTtl) {
    try {
      await _refreshRoomIndex();
    } catch (_) {
      stamp = (await _sync())?.indexFetchedAt;
      if (stamp == null) rethrow;
    }
  }
  // Do not select geometry when searching thousands of lightweight rows.
  final table = _db.campusMapRooms;
  final floors = _db.campusMapFloors;
  final buildings = _db.campusMapBuildings;
  final browsableBuildings = _db.selectOnly(floors)
    ..addColumns([floors.building])
    ..where(floors.code.equals('1F'));
  final searchQuery = _db.selectOnly(table)
    ..addColumns([
      table.id,
      table.layerName,
      table.nameZh,
      table.nameEn,
      table.number,
      floors.code,
      floors.building,
      buildings.name,
    ])
    ..join([
      innerJoin(floors, floors.layerName.equalsExp(table.layerName)),
      innerJoin(buildings, buildings.code.equalsExp(floors.building)),
    ])
    ..where(floors.building.isInQuery(browsableBuildings));
  if (_firstFloorOutlines case final outlines?) {
    searchQuery.where(floors.building.isIn(outlines.keys));
  }
  final exact = <CampusMapRoom>[];
  final partial = <CampusMapRoom>[];
  for (final row in await searchQuery.get()) {
    final room = CampusMapRoom(
      id: row.read(table.id)!,
      buildingCode: row.read(floors.building)!,
      buildingName:
          _firstFloorOutlines?[row.read(floors.building)!]?.name ??
          row.read(buildings.name)!,
      floor: CampusMapFloor(
        layerName: row.read(table.layerName)!,
        code: row.read(floors.code)!,
      ),
      nameZh: row.read(table.nameZh)!,
      nameEn: row.read(table.nameEn)!,
      number: row.read(table.number)!,
    );
    final candidates = [
      room.nameZh,
      room.nameEn,
      room.number,
      '${room.buildingCode}${room.number}',
      '${room.buildingName}${room.number}',
      '${room.buildingName}${room.nameZh}',
      if (buildingNames[room.buildingCode] case final displayName?) ...[
        '$displayName${room.number}',
        '$displayName${room.nameZh}',
        '$displayName${room.nameEn}',
      ],
    ].where((value) => value.isNotEmpty).map(normalizeCampusMapQuery);
    if (candidates.contains(normalized)) {
      exact.add(room);
    } else if (candidates.any((value) => value.contains(normalized))) {
      partial.add(room);
    }
  }
  final matches = exact.isNotEmpty ? exact : partial;
  return matches..sort((a, b) {
    final byBuilding = a.buildingName.compareTo(b.buildingName);
    if (byBuilding != 0) return byBuilding;
    final byFloor = a.floor.order.compareTo(b.floor.order);
    return byFloor == 0 ? a.number.compareTo(b.number) : byFloor;
  });
}