```python
def parse_semver(version: str) -> tuple[int, int, int, str | None]:
    """
    Parses a semantic version string into a tuple of integers and a prerelease string.
    
    Args:
        version (str): The semantic version string to parse.

    Returns:
        tuple[int, int, int, str | None]: A tuple containing the major, minor,
            patch numbers as integers, and the prerelease string if present, or None otherwise.
    """
    parts = version.split('-')
    
    # Check for too few or too many parts
    if len(parts) > 2:
        raise ValueError("Too many parts in semantic version")
    
    major, minor, patch = parts[0].split('.')
    
    # Validate numeric parts
    try:
        major = int(major)
        minor = int(minor)
        patch = int(patch)
    except ValueError as e:
        raise ValueError(f"Invalid numeric part: {e}")
    
    # Check for empty string in numeric parts
    if not major or not minor or not patch:
        raise ValueError("Numeric part cannot be empty")
    
    # Validate prerelease part
    prerelease = None
    if len(parts) == 2:
        prerelease = parts[1]
        
        # Check for invalid characters in prerelease
        if any(char not in '0-9A-Za-z.-' for char in prerelease):
            raise ValueError("Invalid character in prerelease")
    
    return (major, minor, patch, prerelease)
```