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 Type | Description | Example |
|---|
| Listings | Business directory entries | ”Maboneng Coffee”, “Sandton City Mall” |
| Voting Districts | Polling station locations | ”Voting Station 43210098” |
| Community Centers | Public facilities | ”Soweto Recreation Centre” |
| Points of Interest | General landmarks | Parks, 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:
- Load Categories — Fetch existing category taxonomy from
directory.categories
- Load Brands — Read known brand data from
reference_docs/sa-brands.csv
- Load Listings — Parse raw listings from
reference_docs/remaining-listings.csv
- Category Mapping — Match raw categories to slugs using heuristics
- Brand Enrichment — Add logos and websites from brand database
- 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)
Future: Algolia Search
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