Skip to main content

The Directory Engine

@doc-version: 1.0.0 @last-updated: 2026-01-10 The Directory Engine is Visita’s central “Place” database, managing how physical locations map to digital entities. It powers the local business directory, location search, and serves as the foundation for the marketplace.

Overview

Core Principle: The directory unifies diverse location types (businesses, voting stations, community centers) into a single, searchable structure anchored to Wards.

What the Directory Contains

Entity TypeDescriptionExample
ListingsBusiness directory entries”Maboneng Coffee”, “Sandton City Mall”
Voting DistrictsPolling station locations”Voting Station 43210098”
Community CentersPublic facilities”Soweto Recreation Centre”
Points of InterestGeneral landmarksParks, monuments, transit hubs

Architecture

Schema: directory

The directory uses a dedicated PostgreSQL schema to isolate location intelligence from other domains.
-- Core tables in the directory schema
directory.listings      -- Business directory entries
directory.categories    -- Hierarchical category taxonomy
directory.entities      -- Generic location entities (future)

The Listings Table

create table directory.listings (
  id uuid primary key default gen_random_uuid(),
  title text not null,                    -- Display name
  business_name text,                     -- Official business name
  description text,
  category text,                          -- Denormalized category name
  category_id uuid references directory.categories(id),
  
  -- Location
  address text,
  formatted_address text,
  latitude double precision,
  longitude double precision,
  ward_code text references public.wards(ward_code),
  
  -- Contact
  phone text,
  email text,
  website_url text,
  
  -- Media
  image_url text,
  logo_url text,
  
  -- Operations
  opening_hours jsonb,                    -- {"monday": {"open": "08:00", "close": "17:00"}}
  
  -- Status
  is_active boolean default true,
  is_verified boolean default false,
  source_imported_at timestamptz,
  
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Indexes for performance
create index idx_listings_category on directory.listings(category);
create index idx_listings_ward on directory.listings(ward_code);
create index idx_listings_title_search on directory.listings using gin(to_tsvector('english', title));

Category Taxonomy

Categories follow a hierarchical structure for filtering and navigation:
create table directory.categories (
  id uuid primary key default gen_random_uuid(),
  name text not null,                     -- "Food & Beverage"
  slug text unique not null,              -- "food-beverage"
  parent_id uuid references directory.categories(id),
  icon text,                              -- Icon identifier
  display_order int default 0
);
Example Categories:
  • restaurant → Food & Dining
  • retail → Shopping
  • healthcare → Medical Services
  • professional-services → Banks, Lawyers, etc.
  • automotive → Mechanics, Car Dealers

Server Actions

Searching Listings

The primary search action uses pattern matching on title:
// app/actions/directory.ts
export async function searchDirectoryListings(query: string): Promise<DirectoryListingResult[]> {
  if (!query || query.length < 3) return [];

  const supabase = await createClient();

  const { data, error } = await supabase
    .schema('directory')
    .from('listings')
    .select('id, title, description, address, category, formatted_address, image_url')
    .ilike('title', `%${query}%`)
    .limit(10);

  if (error) {
    console.error("Directory search failed:", error);
    return [];
  }
  return data || [];
}

Result Type

export interface DirectoryListingResult {
  id: string;
  title: string;
  description: string | null;
  address: string | null;
  category: string | null;
  formatted_address: string | null;
  image_url: string | null;
}

Data Ingestion

The Ingestion Script

Bulk directory data is imported via scripts/ingest-directory.ts:
npx tsx scripts/ingest-directory.ts
Pipeline Steps:
  1. Load Categories — Fetch existing category taxonomy from directory.categories
  2. Load Brands — Read known brand data from reference_docs/sa-brands.csv
  3. Load Listings — Parse raw listings from reference_docs/remaining-listings.csv
  4. Category Mapping — Match raw categories to slugs using heuristics
  5. Brand Enrichment — Add logos and websites from brand database
  6. Batch Upsert — Insert/update in batches of 1000

Category Mapping Heuristics

The ingestion script maps raw category strings to standardized slugs:
const CATEGORY_MAPPINGS: Record<string, string> = {
  'restaurant': 'restaurant',
  'cafe': 'restaurant',
  'coffee': 'restaurant',
  'retail': 'retail',
  'store': 'retail',
  'shop': 'retail',
  'bank': 'professional-services',
  'doctor': 'healthcare',
  'clinic': 'healthcare',
  'school': 'education',
  'gym': 'sports-facility',
  // ... more mappings
};

Integration with Marketplace

Directory listings can be linked to stores in the business_intelligence schema:
directory.listings  ←──→  business_intelligence.stores

   Display Info              Commerce Operations
   (Name, Hours)             (Products, Orders)
When a seller creates a store, they link it to an existing directory listing:
// Store creation links to a listing
const store = await createStore(listingId, storeName, userId);
This separation ensures:
  • Directory = Public display information (hours, location, description)
  • Store = Commerce operations (inventory, payments, orders)

The directory will be indexed to Algolia for advanced search capabilities:
  • Typo Tolerance — “Maboneg Cofee” → “Maboneng Coffee”
  • Geo-filtering — “cafes near me” with location context
  • Faceted Search — Filter by category, ward, ratings
See Search & Algolia Guide for implementation details.

Status: Active
Last Updated: January 2026